afpnews-mcp-server 1.3.6 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +5 -3
- package/build/server.js +3 -3
- package/build/tools/find-similar.js +22 -9
- package/build/tools/get-article.js +24 -11
- package/build/tools/list-facets.js +20 -9
- package/build/tools/search-articles.js +27 -12
- package/build/tools/shared.js +12 -0
- package/build/utils/format.js +38 -2
- package/build/utils/types.js +5 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -19,10 +19,11 @@ export function resolveStdioAuthConfig(env = process.env) {
|
|
|
19
19
|
const apiKey = env.APICORE_API_KEY?.trim();
|
|
20
20
|
const username = env.APICORE_USERNAME?.trim();
|
|
21
21
|
const password = env.APICORE_PASSWORD?.trim();
|
|
22
|
+
const baseUrl = env.APICORE_BASE_URL?.trim();
|
|
22
23
|
if (!apiKey || !username || !password) {
|
|
23
24
|
throw new Error('Missing stdio auth configuration: set APICORE_API_KEY + APICORE_USERNAME + APICORE_PASSWORD.');
|
|
24
25
|
}
|
|
25
|
-
return { apiKey, username, password };
|
|
26
|
+
return { apiKey, username, password, baseUrl };
|
|
26
27
|
}
|
|
27
28
|
async function startHttpServer() {
|
|
28
29
|
const { default: express } = await import('express');
|
|
@@ -30,6 +31,7 @@ async function startHttpServer() {
|
|
|
30
31
|
if (!apiKey) {
|
|
31
32
|
throw new Error('APICORE_API_KEY environment variable is required');
|
|
32
33
|
}
|
|
34
|
+
const baseUrl = process.env.APICORE_BASE_URL?.trim();
|
|
33
35
|
const app = express();
|
|
34
36
|
const sessions = new Map();
|
|
35
37
|
app.all('/mcp', async (req, res) => {
|
|
@@ -57,7 +59,7 @@ async function startHttpServer() {
|
|
|
57
59
|
const transport = new StreamableHTTPServerTransport({
|
|
58
60
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
59
61
|
});
|
|
60
|
-
const server = await createServer(apiKey, credentials.username, credentials.password);
|
|
62
|
+
const server = await createServer({ apiKey, username: credentials.username, password: credentials.password, baseUrl });
|
|
61
63
|
await server.connect(transport);
|
|
62
64
|
transport.onclose = () => {
|
|
63
65
|
const sid = transport.sessionId;
|
|
@@ -80,7 +82,7 @@ async function startHttpServer() {
|
|
|
80
82
|
}
|
|
81
83
|
async function startStdioServer() {
|
|
82
84
|
const authConfig = resolveStdioAuthConfig();
|
|
83
|
-
const server = await createServer(authConfig
|
|
85
|
+
const server = await createServer(authConfig);
|
|
84
86
|
const transport = new StdioServerTransport();
|
|
85
87
|
await server.connect(transport);
|
|
86
88
|
console.error('MCP stdio server started');
|
package/build/server.js
CHANGED
|
@@ -3,15 +3,15 @@ import { ApiCore } from "afpnews-api";
|
|
|
3
3
|
import { registerTools } from "./tools/index.js";
|
|
4
4
|
import { registerResources } from "./resources/index.js";
|
|
5
5
|
import { registerPrompts } from "./prompts/index.js";
|
|
6
|
-
export async function createServer(apiKey, username, password) {
|
|
6
|
+
export async function createServer({ apiKey, username, password, baseUrl, }) {
|
|
7
7
|
if (!apiKey || !username || !password) {
|
|
8
8
|
throw new Error("Missing authentication configuration. Provide APICORE_API_KEY, APICORE_USERNAME and APICORE_PASSWORD.");
|
|
9
9
|
}
|
|
10
|
-
const apicore = new ApiCore({ apiKey });
|
|
10
|
+
const apicore = new ApiCore({ ...(baseUrl ? { baseUrl } : {}), apiKey });
|
|
11
11
|
await apicore.authenticate({ username, password });
|
|
12
12
|
const server = new McpServer({
|
|
13
13
|
name: "afpnews",
|
|
14
|
-
version: "1.
|
|
14
|
+
version: "1.3.8",
|
|
15
15
|
});
|
|
16
16
|
const ctx = { server, apicore };
|
|
17
17
|
registerTools(ctx);
|
|
@@ -1,36 +1,49 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, toolError, truncateIfNeeded } from '../utils/format.js';
|
|
3
|
-
import { formatDocuments, formatErrorMessage, langEnum, } from './shared.js';
|
|
2
|
+
import { formatDocumentsAsJson, formatDocumentsAsCsv, textContent, toolError, truncateIfNeeded, } from '../utils/format.js';
|
|
3
|
+
import { formatDocuments, formatErrorMessage, langEnum, outputFormatEnum, docFieldEnum, DEFAULT_OUTPUT_FIELDS, UNO_FORMAT_NOTE, } from './shared.js';
|
|
4
4
|
export const afpFindSimilarTool = {
|
|
5
5
|
name: 'afp_find_similar',
|
|
6
6
|
title: 'Find Similar AFP Articles',
|
|
7
7
|
description: `Find AFP news articles similar to a given article (More Like This). Useful for exploring related coverage or finding follow-up stories.
|
|
8
8
|
|
|
9
|
+
${UNO_FORMAT_NOTE}
|
|
10
|
+
|
|
9
11
|
Args:
|
|
10
12
|
- uno: The UNO of the reference article to find similar content for
|
|
11
13
|
- lang: Language for results (e.g. 'en', 'fr')
|
|
12
14
|
- size: Number of similar articles to return (default 10)
|
|
15
|
+
- format: Output format — markdown (default), json, or csv. json/csv omit article body text.
|
|
16
|
+
- fields: Fields to include in json/csv output (default: uno, headline, lang, genre).
|
|
17
|
+
Available: uno, headline, lang, genre, afpshortid, published, status, signal, advisory, country, city, slug, product, revision, created.
|
|
13
18
|
|
|
14
19
|
Returns:
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Article excerpt (first paragraphs)
|
|
20
|
+
- markdown: Summary line + formatted article excerpts
|
|
21
|
+
- json: { total, documents: [...] } with selected fields
|
|
22
|
+
- csv: Header row + data rows with selected fields
|
|
19
23
|
|
|
20
24
|
Examples:
|
|
21
|
-
- Find similar articles in French: { uno: "
|
|
22
|
-
-
|
|
25
|
+
- Find similar articles in French: { uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e", lang: "fr" }
|
|
26
|
+
- Export similar as CSV: { uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e", lang: "en", format: "csv", fields: ["uno", "headline"] }`,
|
|
23
27
|
inputSchema: z.object({
|
|
24
28
|
uno: z.string().describe('The UNO of the reference article'),
|
|
25
29
|
lang: langEnum.describe("Language for results (e.g. 'en', 'fr')"),
|
|
26
30
|
size: z.number().optional().describe('Number of similar articles to return (default 10)'),
|
|
31
|
+
format: outputFormatEnum.optional().describe('Output format: markdown (default, with article excerpt), json (structured, no body), csv (tabular, no body).'),
|
|
32
|
+
fields: docFieldEnum.array().optional().describe('Fields to include in json/csv output. Default: afpshortid, uno, headline, published, lang, genre.'),
|
|
27
33
|
}),
|
|
28
|
-
handler: async (apicore, { uno, lang, size }) => {
|
|
34
|
+
handler: async (apicore, { uno, lang, size, format = 'markdown', fields }) => {
|
|
29
35
|
try {
|
|
30
36
|
const { documents, count } = await apicore.mlt(uno, lang, size);
|
|
31
37
|
if (count === 0) {
|
|
32
38
|
return { content: [textContent('No similar articles found.')] };
|
|
33
39
|
}
|
|
40
|
+
const outputFields = fields ?? DEFAULT_OUTPUT_FIELDS;
|
|
41
|
+
if (format === 'json') {
|
|
42
|
+
return { content: [formatDocumentsAsJson(documents, outputFields, { total: count })] };
|
|
43
|
+
}
|
|
44
|
+
if (format === 'csv') {
|
|
45
|
+
return { content: [formatDocumentsAsCsv(documents, outputFields)] };
|
|
46
|
+
}
|
|
34
47
|
const content = [
|
|
35
48
|
textContent(`*Found ${count} similar articles.*`),
|
|
36
49
|
...formatDocuments(documents, false),
|
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import { formatErrorMessage } from './shared.js';
|
|
2
|
+
import { formatFullArticle, toolError } from '../utils/format.js';
|
|
3
|
+
import { formatErrorMessage, UNO_FORMAT_NOTE } from './shared.js';
|
|
4
4
|
export const afpGetArticleTool = {
|
|
5
5
|
name: 'afp_get_article',
|
|
6
6
|
title: 'Get AFP Article',
|
|
7
|
-
description: `
|
|
7
|
+
description: `Retrieve the complete text of a specific AFP article by its UNO identifier.
|
|
8
|
+
|
|
9
|
+
Use this tool when you have a UNO (from afp_search_articles or afp_find_similar results) and need:
|
|
10
|
+
- The full, untruncated article body
|
|
11
|
+
- All available metadata (country, city, slug, revision, status, signal, advisory)
|
|
12
|
+
- A definitive version of the article before quoting or summarising
|
|
13
|
+
|
|
14
|
+
Do NOT use this to discover articles — use afp_search_articles for that.
|
|
15
|
+
|
|
16
|
+
${UNO_FORMAT_NOTE}
|
|
8
17
|
|
|
9
18
|
Args:
|
|
10
|
-
- uno: The unique identifier (
|
|
19
|
+
- uno: The unique article identifier (e.g. newsml.afp.com.20260222T090659Z.doc-98hu39e)
|
|
11
20
|
|
|
12
21
|
Returns:
|
|
13
|
-
Markdown-formatted
|
|
22
|
+
Markdown-formatted article:
|
|
14
23
|
- ## Headline
|
|
15
|
-
-
|
|
16
|
-
-
|
|
24
|
+
- **UNO:** ...
|
|
25
|
+
- **Lang:** · **Genre:** · **Product:** · **Revision:**
|
|
26
|
+
- **Country:** · **City:** · **Slug:** (when available)
|
|
27
|
+
- **Status:** · **Signal:** · **Advisory:** (when present)
|
|
28
|
+
- ---
|
|
29
|
+
- Full article body (all paragraphs, no truncation)
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
Example:
|
|
32
|
+
{ uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e" }`,
|
|
20
33
|
inputSchema: z.object({
|
|
21
|
-
uno: z.string().describe('The unique identifier
|
|
34
|
+
uno: z.string().describe('The unique UNO identifier of the article (e.g. AFP-FR-2025-12-31-0001)'),
|
|
22
35
|
}),
|
|
23
36
|
handler: async (apicore, { uno }) => {
|
|
24
37
|
try {
|
|
25
38
|
const doc = await apicore.get(uno);
|
|
26
|
-
return { content: [
|
|
39
|
+
return { content: [formatFullArticle(doc)] };
|
|
27
40
|
}
|
|
28
41
|
catch (error) {
|
|
29
42
|
return toolError(formatErrorMessage(`fetching article "${uno}"`, error, 'Verify the UNO identifier is correct.'));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textContent, toolError } from '../utils/format.js';
|
|
3
|
-
import { formatErrorMessage, langEnum, listPresetEnum, } from './shared.js';
|
|
3
|
+
import { formatErrorMessage, langEnum, listPresetEnum, outputFormatEnum, } from './shared.js';
|
|
4
4
|
export const afpListFacetsTool = {
|
|
5
5
|
name: 'afp_list_facets',
|
|
6
6
|
title: 'List AFP Facet Values',
|
|
@@ -11,23 +11,26 @@ Args:
|
|
|
11
11
|
- facet: Facet to list (e.g. 'slug', 'genre', 'country'). Required when no preset is used.
|
|
12
12
|
- lang: Language filter (e.g. 'en', 'fr')
|
|
13
13
|
- size: Number of facet values to return
|
|
14
|
+
- format: Output format — markdown (default), json, or csv.
|
|
14
15
|
|
|
15
16
|
Returns:
|
|
16
|
-
|
|
17
|
-
-
|
|
17
|
+
- markdown: Formatted list with labels and article counts
|
|
18
|
+
- json: Array of { name, count } objects
|
|
19
|
+
- csv: name,count rows
|
|
18
20
|
|
|
19
21
|
Examples:
|
|
20
22
|
- Trending topics in French: { preset: "trending-topics" }
|
|
21
23
|
- Trending topics in English: { preset: "trending-topics", lang: "en" }
|
|
22
|
-
- List available genres: { facet: "genre" }
|
|
23
|
-
- List countries: { facet: "country", size: 30 }`,
|
|
24
|
+
- List available genres as CSV: { facet: "genre", format: "csv" }
|
|
25
|
+
- List countries as JSON: { facet: "country", size: 30, format: "json" }`,
|
|
24
26
|
inputSchema: z.object({
|
|
25
27
|
preset: listPresetEnum.optional().describe('Optional preset for list queries. Available preset: trending-topics.'),
|
|
26
28
|
facet: z.string().optional().describe("Facet to list (e.g. 'slug', 'genre', 'country'). Required when no preset is used."),
|
|
27
29
|
lang: langEnum.optional().describe("Language filter (e.g. 'en', 'fr')"),
|
|
28
30
|
size: z.number().optional().describe('Number of facet values to return'),
|
|
31
|
+
format: outputFormatEnum.optional().describe('Output format: markdown (default), json, or csv.'),
|
|
29
32
|
}),
|
|
30
|
-
handler: async (apicore, { preset, facet, lang, size }) => {
|
|
33
|
+
handler: async (apicore, { preset, facet, lang, size, format = 'markdown' }) => {
|
|
31
34
|
try {
|
|
32
35
|
const isTrendingTopics = preset === 'trending-topics';
|
|
33
36
|
const resolvedFacet = isTrendingTopics ? 'slug' : facet;
|
|
@@ -42,10 +45,18 @@ Examples:
|
|
|
42
45
|
if (results.length === 0) {
|
|
43
46
|
return { content: [textContent(`No facet values found for "${resolvedFacet}".`)] };
|
|
44
47
|
}
|
|
48
|
+
if (format === 'json') {
|
|
49
|
+
return { content: [textContent(JSON.stringify(results, null, 2))] };
|
|
50
|
+
}
|
|
51
|
+
if (format === 'csv') {
|
|
52
|
+
const rows = results.map(r => {
|
|
53
|
+
const name = /[",\n\r]/.test(r.name) ? `"${r.name.replaceAll('"', '""')}"` : r.name;
|
|
54
|
+
return `${name},${r.count}`;
|
|
55
|
+
});
|
|
56
|
+
return { content: [textContent(['name,count', ...rows].join('\n'))] };
|
|
57
|
+
}
|
|
45
58
|
const heading = isTrendingTopics ? 'Trending Topics' : `Facet: ${resolvedFacet}`;
|
|
46
|
-
const lines = results.map((item) => {
|
|
47
|
-
return `- **${item.name}** — ${item.count} articles`;
|
|
48
|
-
});
|
|
59
|
+
const lines = results.map((item) => `- **${item.name}** — ${item.count} articles`);
|
|
49
60
|
return { content: [textContent(`## ${heading}\n\n${lines.join('\n')}`)] };
|
|
50
61
|
}
|
|
51
62
|
catch (error) {
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { DEFAULT_FIELDS, GENRE_EXCLUSIONS, textContent, toolError, truncateIfNeeded, buildPaginationLine, } from '../utils/format.js';
|
|
2
|
+
import { DEFAULT_FIELDS, GENRE_EXCLUSIONS, formatDocumentsAsJson, formatDocumentsAsCsv, textContent, toolError, truncateIfNeeded, buildPaginationLine, } from '../utils/format.js';
|
|
3
3
|
import { DEFAULT_SEARCH_SIZE } from '../utils/types.js';
|
|
4
|
-
import { SEARCH_PRESETS, formatErrorMessage, formatDocuments, langEnum, searchPresetEnum, } from './shared.js';
|
|
4
|
+
import { SEARCH_PRESETS, formatErrorMessage, formatDocuments, langEnum, searchPresetEnum, outputFormatEnum, docFieldEnum, DEFAULT_OUTPUT_FIELDS, UNO_FORMAT_NOTE, } from './shared.js';
|
|
5
5
|
export const afpSearchArticlesTool = {
|
|
6
6
|
name: 'afp_search_articles',
|
|
7
7
|
title: 'Search AFP News Articles',
|
|
8
8
|
description: `Search AFP news articles with filters and presets. This is the primary query tool for all AFP news search use cases.
|
|
9
9
|
|
|
10
|
+
${UNO_FORMAT_NOTE}
|
|
11
|
+
|
|
10
12
|
Args:
|
|
11
13
|
- preset: Optional predefined filter set (a-la-une, agenda, previsions, major-stories)
|
|
12
|
-
-
|
|
14
|
+
- format: Output format — markdown (default), json, or csv. json/csv omit article body text.
|
|
15
|
+
- fields: Fields to include in json/csv output (default: uno, headline, lang, genre).
|
|
16
|
+
Available: uno, headline, lang, genre, afpshortid, published, status, signal, advisory, country, city, slug, product, revision, created.
|
|
17
|
+
- fullText: Return full article body (true) or excerpt only (false, default). Only applies to markdown. Presets override to true.
|
|
13
18
|
- query: Search keywords in the language specified by 'lang' (e.g. 'climate change')
|
|
14
19
|
- lang: Article language filter (e.g. ['en', 'fr']). Use ['en'] for photos.
|
|
15
20
|
- dateFrom/dateTo: Date range in ISO format (e.g. '2025-01-01') or relative ('now-1d')
|
|
@@ -21,19 +26,19 @@ Args:
|
|
|
21
26
|
- product: Content type filter (default ['news', 'factcheck'])
|
|
22
27
|
|
|
23
28
|
Returns:
|
|
24
|
-
Pagination summary line
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- Article body (excerpt or full text)
|
|
29
|
+
- markdown: Pagination summary line + formatted articles with headline, metadata, body
|
|
30
|
+
- json: { total, shown, offset, documents: [...] } with selected fields
|
|
31
|
+
- csv: Header row + data rows with selected fields
|
|
28
32
|
|
|
29
33
|
Examples:
|
|
30
34
|
- Latest Ukraine news: { query: "Ukraine", lang: ["en"], size: 5 }
|
|
31
35
|
- French front page: { preset: "a-la-une" }
|
|
32
|
-
-
|
|
33
|
-
- Page 2 of results: { query: "economy", size: 10, offset: 10 }`,
|
|
36
|
+
- Export metadata as CSV: { query: "economy", format: "csv", fields: ["uno", "headline", "country"] }`,
|
|
34
37
|
inputSchema: z.object({
|
|
35
38
|
preset: searchPresetEnum.optional().describe('Optional preset that applies predefined AFP filters. Available presets: a-la-une, agenda, previsions, major-stories.'),
|
|
36
|
-
|
|
39
|
+
format: outputFormatEnum.optional().describe('Output format: markdown (default, with article body), json (structured, no body), csv (tabular, no body).'),
|
|
40
|
+
fields: docFieldEnum.array().optional().describe('Fields to include in json/csv output. Default: afpshortid, uno, headline, published, lang, genre.'),
|
|
41
|
+
fullText: z.boolean().optional().describe('When true, returns the full article body (markdown only). Default is false. Presets override to true.'),
|
|
37
42
|
query: z.string().optional().describe("List of keywords to search for in the news articles (e.g. 'climate change'), in the language specified by the 'lang' parameter. If not specified, the search will be performed in all languages. Do not use keywords in multiple languages."),
|
|
38
43
|
lang: langEnum.array().optional().describe("Language of the news articles (e.g. 'en', 'fr'). Always use 'en' if you look for photos."),
|
|
39
44
|
dateFrom: z.string().optional().describe("Start date for the search in ISO format (e.g. '2023-01-01') or relative (e.g. 'now-1d')"),
|
|
@@ -45,7 +50,7 @@ Examples:
|
|
|
45
50
|
slug: z.string().array().optional().describe("Topic/slug filter (e.g. 'economy', 'sports')"),
|
|
46
51
|
product: z.enum(['news', 'factcheck', 'photo', 'video', 'multimedia', 'graphic', 'videographic']).array().optional().describe("Content type filter (default ['news', 'factcheck'])"),
|
|
47
52
|
}),
|
|
48
|
-
handler: async (apicore, { preset, fullText = false, query, lang, dateFrom, dateTo, size = DEFAULT_SEARCH_SIZE, sortOrder = 'desc', offset, country, slug, product = ['news', 'factcheck'] }) => {
|
|
53
|
+
handler: async (apicore, { preset, format = 'markdown', fields, fullText = false, query, lang, dateFrom, dateTo, size = DEFAULT_SEARCH_SIZE, sortOrder = 'desc', offset, country, slug, product = ['news', 'factcheck'] }) => {
|
|
49
54
|
try {
|
|
50
55
|
let request = {
|
|
51
56
|
query,
|
|
@@ -64,11 +69,21 @@ Examples:
|
|
|
64
69
|
request = { ...request, ...SEARCH_PRESETS[preset] };
|
|
65
70
|
fullText = true;
|
|
66
71
|
}
|
|
67
|
-
const
|
|
72
|
+
const outputFields = fields ?? DEFAULT_OUTPUT_FIELDS;
|
|
73
|
+
const apiFields = format === 'markdown'
|
|
74
|
+
? [...DEFAULT_FIELDS]
|
|
75
|
+
: [...new Set(['afpshortid', 'uno', ...outputFields])];
|
|
76
|
+
const { documents, count } = await apicore.search(request, apiFields);
|
|
68
77
|
if (count === 0) {
|
|
69
78
|
return { content: [textContent('No results found.')] };
|
|
70
79
|
}
|
|
71
80
|
const currentOffset = offset ?? 0;
|
|
81
|
+
if (format === 'json') {
|
|
82
|
+
return { content: [formatDocumentsAsJson(documents, outputFields, { total: count, shown: documents.length, offset: currentOffset })] };
|
|
83
|
+
}
|
|
84
|
+
if (format === 'csv') {
|
|
85
|
+
return { content: [formatDocumentsAsCsv(documents, outputFields)] };
|
|
86
|
+
}
|
|
72
87
|
const content = [
|
|
73
88
|
textContent(buildPaginationLine(documents.length, count, currentOffset)),
|
|
74
89
|
...formatDocuments(documents, fullText),
|
package/build/tools/shared.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { formatDocument, GENRE_EXCLUSIONS, } from '../utils/format.js';
|
|
3
|
+
import { ALL_DOC_FIELDS, DEFAULT_OUTPUT_FIELDS } from '../utils/types.js';
|
|
4
|
+
export { ALL_DOC_FIELDS, DEFAULT_OUTPUT_FIELDS };
|
|
5
|
+
// UNO format: newsml.afp.com.20260222T090659Z.doc-98hu39e
|
|
6
|
+
// - timestamp: 20260222T090659Z → 2026-02-22 09:06:59 UTC
|
|
7
|
+
// - afpshortid: the segment after "doc-" (e.g. 98hu39e)
|
|
8
|
+
// Published date and afpshortid are encoded in the UNO — no need to fetch them as separate fields.
|
|
9
|
+
export const UNO_FORMAT_NOTE = `Note on the UNO identifier (e.g. newsml.afp.com.20260222T090659Z.doc-98hu39e):
|
|
10
|
+
- Publication date: the timestamp segment, e.g. 20260222T090659Z → 2026-02-22 09:06:59 UTC
|
|
11
|
+
- Short ID (afpshortid): the segment after "doc-", e.g. 98hu39e
|
|
12
|
+
Both are embedded in the UNO — request afpshortid or published as explicit fields only if needed.`;
|
|
13
|
+
export const outputFormatEnum = z.enum(['markdown', 'json', 'csv']);
|
|
14
|
+
export const docFieldEnum = z.enum(ALL_DOC_FIELDS);
|
|
3
15
|
const SEARCH_PRESET_VALUES = ['a-la-une', 'agenda', 'previsions', 'major-stories'];
|
|
4
16
|
export const searchPresetEnum = z.enum(SEARCH_PRESET_VALUES);
|
|
5
17
|
const LIST_PRESET_VALUES = ['trending-topics'];
|
package/build/utils/format.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { EXCERPT_PARAGRAPH_COUNT, CHARACTER_LIMIT } from './types.js';
|
|
2
|
+
function escapeCsvValue(value) {
|
|
3
|
+
const str = Array.isArray(value) ? value.join('|') : String(value ?? '');
|
|
4
|
+
if (/[",\n\r]/.test(str))
|
|
5
|
+
return `"${str.replaceAll('"', '""')}"`;
|
|
6
|
+
return str;
|
|
7
|
+
}
|
|
8
|
+
export function pickDocFields(doc, fields) {
|
|
9
|
+
const d = doc;
|
|
10
|
+
return Object.fromEntries(fields.map(f => [f, d[f] ?? null]));
|
|
11
|
+
}
|
|
12
|
+
export function formatDocumentsAsJson(docs, fields, meta = {}) {
|
|
13
|
+
const documents = docs.map(doc => pickDocFields(doc, fields));
|
|
14
|
+
return textContent(JSON.stringify({ ...meta, documents }, null, 2));
|
|
15
|
+
}
|
|
16
|
+
export function formatDocumentsAsCsv(docs, fields) {
|
|
17
|
+
const rows = docs.map(doc => fields.map(f => escapeCsvValue(doc[f])).join(','));
|
|
18
|
+
return textContent([fields.join(','), ...rows].join('\n'));
|
|
19
|
+
}
|
|
2
20
|
export const GENRE_EXCLUSIONS = {
|
|
3
21
|
exclude: [
|
|
4
22
|
'afpgenre:Agenda',
|
|
@@ -11,12 +29,11 @@ export const GENRE_EXCLUSIONS = {
|
|
|
11
29
|
'afpattribute:PictureProgram'
|
|
12
30
|
]
|
|
13
31
|
};
|
|
14
|
-
export const DEFAULT_FIELDS = ['uno', 'status', 'signal', 'advisory', '
|
|
32
|
+
export const DEFAULT_FIELDS = ['uno', 'status', 'signal', 'advisory', 'headline', 'news', 'lang', 'genre'];
|
|
15
33
|
export function formatDocument(doc, fullText = false) {
|
|
16
34
|
const d = doc;
|
|
17
35
|
const meta = [
|
|
18
36
|
`UNO: ${d.uno}`,
|
|
19
|
-
`Published: ${typeof d.published === 'string' ? d.published : new Date(d.published).toISOString()}`,
|
|
20
37
|
`Lang: ${d.lang}`,
|
|
21
38
|
`Genre: ${d.genre}`,
|
|
22
39
|
];
|
|
@@ -33,6 +50,25 @@ export function formatDocument(doc, fullText = false) {
|
|
|
33
50
|
const text = `## ${d.headline}\n*${meta.join(' | ')}*\n\n${body}`;
|
|
34
51
|
return { type: 'text', text };
|
|
35
52
|
}
|
|
53
|
+
export function formatFullArticle(doc) {
|
|
54
|
+
const d = doc;
|
|
55
|
+
const row = (...pairs) => pairs
|
|
56
|
+
.filter(([, v]) => v != null && v !== '')
|
|
57
|
+
.map(([k, v]) => `**${k}:** ${Array.isArray(v) ? v.join(', ') : v}`)
|
|
58
|
+
.join(' · ');
|
|
59
|
+
const lines = [];
|
|
60
|
+
lines.push(row(['UNO', d.uno]));
|
|
61
|
+
lines.push(row(['Lang', d.lang], ['Genre', d.genre], ['Product', d.product], ['Revision', d.revision]));
|
|
62
|
+
const extras = row(['Country', d.country], ['City', d.city], ['Slug', d.slug]);
|
|
63
|
+
if (extras)
|
|
64
|
+
lines.push(extras);
|
|
65
|
+
const flags = row(['Status', d.status], ['Signal', d.signal], ['Advisory', d.advisory]);
|
|
66
|
+
if (flags)
|
|
67
|
+
lines.push(flags);
|
|
68
|
+
const meta = lines.join('\n');
|
|
69
|
+
const body = (Array.isArray(d.news) ? d.news : []).join('\n\n');
|
|
70
|
+
return { type: 'text', text: `## ${d.headline}\n\n${meta}\n\n---\n\n${body}` };
|
|
71
|
+
}
|
|
36
72
|
export function textContent(text) {
|
|
37
73
|
return { type: 'text', text };
|
|
38
74
|
}
|
package/build/utils/types.js
CHANGED
|
@@ -2,3 +2,8 @@ export const EXCERPT_PARAGRAPH_COUNT = 4;
|
|
|
2
2
|
export const CHARACTER_LIMIT = 25_000;
|
|
3
3
|
export const DEFAULT_SEARCH_SIZE = 10;
|
|
4
4
|
export const DEFAULT_FACET_SIZE = 20;
|
|
5
|
+
export const ALL_DOC_FIELDS = [
|
|
6
|
+
'afpshortid', 'uno', 'headline', 'published', 'lang', 'genre',
|
|
7
|
+
'status', 'signal', 'advisory', 'country', 'city', 'slug', 'product', 'revision', 'created',
|
|
8
|
+
];
|
|
9
|
+
export const DEFAULT_OUTPUT_FIELDS = ['uno', 'headline', 'lang', 'genre'];
|