@mrtdown/core 2.0.0-alpha.3 → 2.0.0-alpha.4

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.
Files changed (109) hide show
  1. package/dist/cli/commands/create.d.ts +30 -0
  2. package/dist/cli/commands/create.js +189 -0
  3. package/dist/cli/commands/create.js.map +1 -0
  4. package/dist/cli/commands/list.d.ts +6 -0
  5. package/dist/cli/commands/list.js +106 -0
  6. package/dist/cli/commands/list.js.map +1 -0
  7. package/dist/cli/commands/show.d.ts +6 -0
  8. package/dist/cli/commands/show.js +156 -0
  9. package/dist/cli/commands/show.js.map +1 -0
  10. package/dist/cli/commands/validate.d.ts +6 -0
  11. package/dist/cli/commands/validate.js +19 -0
  12. package/dist/cli/commands/validate.js.map +1 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +162 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/llm/client.d.ts +2 -0
  17. package/dist/llm/client.js +5 -0
  18. package/dist/llm/client.js.map +1 -0
  19. package/dist/llm/common/MemoryStore.d.ts +21 -0
  20. package/dist/llm/common/MemoryStore.js +100 -0
  21. package/dist/llm/common/MemoryStore.js.map +1 -0
  22. package/dist/llm/common/MemoryStore.test.d.ts +1 -0
  23. package/dist/llm/common/MemoryStore.test.js +225 -0
  24. package/dist/llm/common/MemoryStore.test.js.map +1 -0
  25. package/dist/llm/common/formatCurrentState.d.ts +10 -0
  26. package/dist/llm/common/formatCurrentState.js +342 -0
  27. package/dist/llm/common/formatCurrentState.js.map +1 -0
  28. package/dist/llm/common/tool.d.ts +32 -0
  29. package/dist/llm/common/tool.js +6 -0
  30. package/dist/llm/common/tool.js.map +1 -0
  31. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.d.ts +1 -0
  32. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js +433 -0
  33. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js.map +1 -0
  34. package/dist/llm/functions/extractClaimsFromNewEvidence/index.d.ts +18 -0
  35. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js +153 -0
  36. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js.map +1 -0
  37. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.d.ts +1 -0
  38. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js +168 -0
  39. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js.map +1 -0
  40. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.d.ts +19 -0
  41. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js +65 -0
  42. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js.map +1 -0
  43. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.d.ts +21 -0
  44. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js +115 -0
  45. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js.map +1 -0
  46. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.d.ts +24 -0
  47. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js +110 -0
  48. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js.map +1 -0
  49. package/dist/llm/functions/generateIssueTitleAndSlug/index.d.ts +14 -0
  50. package/dist/llm/functions/generateIssueTitleAndSlug/index.js +38 -0
  51. package/dist/llm/functions/generateIssueTitleAndSlug/index.js.map +1 -0
  52. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.d.ts +1 -0
  53. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js +23 -0
  54. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js.map +1 -0
  55. package/dist/llm/functions/translate/index.d.ts +1 -0
  56. package/dist/llm/functions/translate/index.js +59 -0
  57. package/dist/llm/functions/translate/index.js.map +1 -0
  58. package/dist/llm/functions/triageNewEvidence/eval.test.d.ts +1 -0
  59. package/dist/llm/functions/triageNewEvidence/eval.test.js +139 -0
  60. package/dist/llm/functions/triageNewEvidence/eval.test.js.map +1 -0
  61. package/dist/llm/functions/triageNewEvidence/index.d.ts +37 -0
  62. package/dist/llm/functions/triageNewEvidence/index.js +121 -0
  63. package/dist/llm/functions/triageNewEvidence/index.js.map +1 -0
  64. package/dist/llm/functions/triageNewEvidence/prompt.d.ts +1 -0
  65. package/dist/llm/functions/triageNewEvidence/prompt.js +60 -0
  66. package/dist/llm/functions/triageNewEvidence/prompt.js.map +1 -0
  67. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.d.ts +19 -0
  68. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js +65 -0
  69. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js.map +1 -0
  70. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.d.ts +19 -0
  71. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js +37 -0
  72. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js.map +1 -0
  73. package/dist/scripts/ingestViaWebhook.d.ts +1 -0
  74. package/dist/scripts/ingestViaWebhook.js +9 -0
  75. package/dist/scripts/ingestViaWebhook.js.map +1 -0
  76. package/dist/validators/buildContext.d.ts +7 -0
  77. package/dist/validators/buildContext.js +164 -0
  78. package/dist/validators/buildContext.js.map +1 -0
  79. package/dist/validators/index.d.ts +17 -0
  80. package/dist/validators/index.js +58 -0
  81. package/dist/validators/index.js.map +1 -0
  82. package/dist/validators/issue.d.ts +13 -0
  83. package/dist/validators/issue.js +220 -0
  84. package/dist/validators/issue.js.map +1 -0
  85. package/dist/validators/landmark.d.ts +7 -0
  86. package/dist/validators/landmark.js +43 -0
  87. package/dist/validators/landmark.js.map +1 -0
  88. package/dist/validators/line.d.ts +8 -0
  89. package/dist/validators/line.js +87 -0
  90. package/dist/validators/line.js.map +1 -0
  91. package/dist/validators/operator.d.ts +7 -0
  92. package/dist/validators/operator.js +43 -0
  93. package/dist/validators/operator.js.map +1 -0
  94. package/dist/validators/service.d.ts +8 -0
  95. package/dist/validators/service.js +87 -0
  96. package/dist/validators/service.js.map +1 -0
  97. package/dist/validators/station.d.ts +8 -0
  98. package/dist/validators/station.js +93 -0
  99. package/dist/validators/station.js.map +1 -0
  100. package/dist/validators/town.d.ts +7 -0
  101. package/dist/validators/town.js +43 -0
  102. package/dist/validators/town.js.map +1 -0
  103. package/dist/validators/types.d.ts +19 -0
  104. package/dist/validators/types.js +2 -0
  105. package/dist/validators/types.js.map +1 -0
  106. package/dist/validators/utils.d.ts +2 -0
  107. package/dist/validators/utils.js +9 -0
  108. package/dist/validators/utils.js.map +1 -0
  109. package/package.json +11 -7
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ declare const ResponseSchema: z.ZodObject<{
3
+ title: z.ZodString;
4
+ slug: z.ZodString;
5
+ }, z.core.$strip>;
6
+ export type GenerateIssueTitleAndSlugParams = {
7
+ text: string;
8
+ };
9
+ export type GenerateIssueTitleAndSlugResult = z.infer<typeof ResponseSchema>;
10
+ /**
11
+ * Generate a title and slug for the given text.
12
+ */
13
+ export declare function generateIssueTitleAndSlug(params: GenerateIssueTitleAndSlugParams): Promise<GenerateIssueTitleAndSlugResult>;
14
+ export {};
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { openAiClient } from '#llm/client.js';
3
+ import { assert } from '#util/assert.js';
4
+ import { buildSystemPrompt } from './prompt.js';
5
+ const ResponseSchema = z.object({
6
+ title: z.string(),
7
+ slug: z.string(),
8
+ });
9
+ /**
10
+ * Generate a title and slug for the given text.
11
+ */
12
+ export async function generateIssueTitleAndSlug(params) {
13
+ const systemPrompt = buildSystemPrompt();
14
+ const context = [
15
+ {
16
+ role: 'user',
17
+ content: `
18
+ Text: ${params.text}
19
+ `.trim(),
20
+ },
21
+ ];
22
+ const response = await openAiClient.responses.parse({
23
+ model: 'gpt-5-nano',
24
+ input: context,
25
+ instructions: systemPrompt,
26
+ text: {
27
+ format: {
28
+ type: 'json_schema',
29
+ name: 'Response',
30
+ strict: true,
31
+ schema: z.toJSONSchema(ResponseSchema),
32
+ },
33
+ },
34
+ });
35
+ assert(response.output_parsed != null, 'Response output parsed is null');
36
+ return response.output_parsed;
37
+ }
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["llm/functions/generateIssueTitleAndSlug/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;CACjB,CAAC,CAAC;AAQH;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAuC;IAEvC,MAAM,YAAY,GAAG,iBAAiB,EAAE,CAAC;IAEzC,MAAM,OAAO,GAAwB;QACnC;YACE,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE;QACP,MAAM,CAAC,IAAI;CAClB,CAAC,IAAI,EAAE;SACH;KACF,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC;QAClD,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,OAAO;QACd,YAAY,EAAE,YAAY;QAC1B,IAAI,EAAE;YACJ,MAAM,EAAE;gBACN,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,UAAU;gBAChB,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,cAAc,CAAC;aACvC;SACF;KACF,CAAC,CAAC;IAEH,MAAM,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI,EAAE,gCAAgC,CAAC,CAAC;IAEzE,OAAO,QAAQ,CAAC,aAAa,CAAC;AAChC,CAAC","sourcesContent":["import type { ResponseInputItem } from 'openai/resources/responses/responses.mjs';\nimport { z } from 'zod';\nimport { openAiClient } from '#llm/client.js';\nimport { assert } from '#util/assert.js';\nimport { buildSystemPrompt } from './prompt.js';\n\nconst ResponseSchema = z.object({\n title: z.string(),\n slug: z.string(),\n});\n\nexport type GenerateIssueTitleAndSlugParams = {\n text: string;\n};\n\nexport type GenerateIssueTitleAndSlugResult = z.infer<typeof ResponseSchema>;\n\n/**\n * Generate a title and slug for the given text.\n */\nexport async function generateIssueTitleAndSlug(\n params: GenerateIssueTitleAndSlugParams,\n): Promise<GenerateIssueTitleAndSlugResult> {\n const systemPrompt = buildSystemPrompt();\n\n const context: ResponseInputItem[] = [\n {\n role: 'user',\n content: `\nText: ${params.text}\n`.trim(),\n },\n ];\n\n const response = await openAiClient.responses.parse({\n model: 'gpt-5-nano',\n input: context,\n instructions: systemPrompt,\n text: {\n format: {\n type: 'json_schema',\n name: 'Response',\n strict: true,\n schema: z.toJSONSchema(ResponseSchema),\n },\n },\n });\n\n assert(response.output_parsed != null, 'Response output parsed is null');\n\n return response.output_parsed;\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function buildSystemPrompt(): string;
@@ -0,0 +1,23 @@
1
+ export function buildSystemPrompt() {
2
+ return `
3
+ You are an AI assistant helping to process MRT/LRT incident data for Singapore's public transport system. Your task is to generate a clear title and URL slug for the given issue based on the provided text.
4
+
5
+ ## Your Responsibilities
6
+
7
+ ### 1. Title Creation
8
+ - Create clear, factual titles describing the disruption, maintenance, or infrastructure issue
9
+ - Format: "[Line Code] [Type of Issue] - [Location/Scope]" when line and scope are known
10
+ - Examples: "NSL Signalling Fault - Ang Mo Kio to Bishan", "EWL Train Breakdown - Clementi Station", "Circle Line: Service disruption due to track fault"
11
+ - Avoid sensational language; stick to facts
12
+ - Use English (en-SG) as the output language
13
+
14
+ ### 2. Slug Generation
15
+ - Slug will be prefixed with YYYY-MM-DD by the system (e.g., "2024-01-15-nsl-signalling-fault")
16
+ - Generate only the descriptive part (no date)
17
+ - Rules: lowercase, hyphen-separated, no spaces, valid URL slug
18
+ - Keep slugs concise but descriptive enough to distinguish similar issues
19
+ - Include key identifiers: line, issue type, location or cause when relevant
20
+ - Examples: "nsl-signalling-fault", "circle-line-track-fault-holland-village-caldecott", "faulty-cable-led-to-circle-line-disruption"
21
+ `.trim();
22
+ }
23
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"/","sources":["llm/functions/generateIssueTitleAndSlug/prompt.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;CAmBR,CAAC,IAAI,EAAE,CAAC;AACT,CAAC","sourcesContent":["export function buildSystemPrompt() {\n return `\nYou are an AI assistant helping to process MRT/LRT incident data for Singapore's public transport system. Your task is to generate a clear title and URL slug for the given issue based on the provided text.\n\n## Your Responsibilities\n\n### 1. Title Creation\n- Create clear, factual titles describing the disruption, maintenance, or infrastructure issue\n- Format: \"[Line Code] [Type of Issue] - [Location/Scope]\" when line and scope are known\n- Examples: \"NSL Signalling Fault - Ang Mo Kio to Bishan\", \"EWL Train Breakdown - Clementi Station\", \"Circle Line: Service disruption due to track fault\"\n- Avoid sensational language; stick to facts\n- Use English (en-SG) as the output language\n\n### 2. Slug Generation\n- Slug will be prefixed with YYYY-MM-DD by the system (e.g., \"2024-01-15-nsl-signalling-fault\")\n- Generate only the descriptive part (no date)\n- Rules: lowercase, hyphen-separated, no spaces, valid URL slug\n- Keep slugs concise but descriptive enough to distinguish similar issues\n- Include key identifiers: line, issue type, location or cause when relevant\n- Examples: \"nsl-signalling-fault\", \"circle-line-track-fault-holland-village-caldecott\", \"faulty-cable-led-to-circle-line-disruption\"\n`.trim();\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function translate(text: string): Promise<never>;
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { estimateOpenAICostFromUsage, normalizeOpenAIResponsesUsage, } from '#helpers/estimateOpenAICost.js';
3
+ import { openAiClient } from '#llm/client.js';
4
+ import { TranslationsSchema } from '#schema/common.js';
5
+ import { assert } from '#util/assert.js';
6
+ export async function translate(text) {
7
+ console.log('[translate] Translating text:', text);
8
+ const model = 'gpt-5-nano';
9
+ const context = [{ role: 'user', content: text }];
10
+ const response = await openAiClient.responses.parse({
11
+ model,
12
+ input: context,
13
+ instructions: `You are a helpful assistant that translates text to the following languages:
14
+ - English
15
+ - Chinese (Simplified)
16
+ - Malay
17
+ - Tamil
18
+
19
+ These translations relate to transit disruption/maintenance/infrastructure issues and can contain names of lines and/or stations.
20
+ Keep the names in English as much as possible, do not provide any translations for them.
21
+ `.trim(),
22
+ text: {
23
+ format: {
24
+ type: 'json_schema',
25
+ name: 'Translation',
26
+ strict: true,
27
+ schema: z.toJSONSchema(TranslationsSchema),
28
+ },
29
+ },
30
+ reasoning: {
31
+ effort: 'minimal',
32
+ summary: 'concise',
33
+ },
34
+ store: false,
35
+ include: ['reasoning.encrypted_content'],
36
+ });
37
+ const usage = normalizeOpenAIResponsesUsage(response.usage);
38
+ const estimate = estimateOpenAICostFromUsage({ model, usage });
39
+ if (usage != null) {
40
+ console.log('[translate] Usage:', {
41
+ inputTokens: usage.inputTokens,
42
+ cachedInputTokens: usage.cachedInputTokens,
43
+ outputTokens: usage.outputTokens,
44
+ totalTokens: usage.totalTokens,
45
+ });
46
+ if (estimate != null) {
47
+ console.log('[translate] Estimated cost (USD):', estimate.estimatedCostUsd.toFixed(8));
48
+ }
49
+ else {
50
+ console.log(`[translate] No pricing configured for model "${model}".`);
51
+ }
52
+ }
53
+ else {
54
+ console.log('[translate] Usage is unavailable');
55
+ }
56
+ assert(response.output_parsed != null, 'Response output parsed is null');
57
+ return response.output_parsed;
58
+ }
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["llm/functions/translate/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,OAAO,CAAC,GAAG,CAAC,+BAA+B,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,YAAY,CAAC;IAE3B,MAAM,OAAO,GAAwB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvE,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC;QAClD,KAAK;QACL,KAAK,EAAE,OAAO;QACd,YAAY,EACV;;;;;;;;CAQL,CAAC,IAAI,EAAE;QACJ,IAAI,EAAE;YACJ,MAAM,EAAE;gBACN,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,kBAAkB,CAAC;aAC3C;SACF;QACD,SAAS,EAAE;YACT,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,SAAS;SACnB;QACD,KAAK,EAAE,KAAK;QACZ,OAAO,EAAE,CAAC,6BAA6B,CAAC;KACzC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,6BAA6B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,2BAA2B,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE;YAChC,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;YAC1C,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAAC,CAAC;QACH,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CACT,mCAAmC,EACnC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CACrC,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,gDAAgD,KAAK,IAAI,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI,EAAE,gCAAgC,CAAC,CAAC;IAEzE,OAAO,QAAQ,CAAC,aAAa,CAAC;AAChC,CAAC","sourcesContent":["import type { ResponseInputItem } from 'openai/resources/responses/responses.js';\nimport { z } from 'zod';\nimport {\n estimateOpenAICostFromUsage,\n normalizeOpenAIResponsesUsage,\n} from '#helpers/estimateOpenAICost.js';\nimport { openAiClient } from '#llm/client.js';\nimport { TranslationsSchema } from '#schema/common.js';\nimport { assert } from '#util/assert.js';\n\nexport async function translate(text: string) {\n console.log('[translate] Translating text:', text);\n const model = 'gpt-5-nano';\n\n const context: ResponseInputItem[] = [{ role: 'user', content: text }];\n\n const response = await openAiClient.responses.parse({\n model,\n input: context,\n instructions:\n `You are a helpful assistant that translates text to the following languages:\n- English\n- Chinese (Simplified)\n- Malay\n- Tamil\n\nThese translations relate to transit disruption/maintenance/infrastructure issues and can contain names of lines and/or stations.\nKeep the names in English as much as possible, do not provide any translations for them.\n`.trim(),\n text: {\n format: {\n type: 'json_schema',\n name: 'Translation',\n strict: true,\n schema: z.toJSONSchema(TranslationsSchema),\n },\n },\n reasoning: {\n effort: 'minimal',\n summary: 'concise',\n },\n store: false,\n include: ['reasoning.encrypted_content'],\n });\n\n const usage = normalizeOpenAIResponsesUsage(response.usage);\n const estimate = estimateOpenAICostFromUsage({ model, usage });\n if (usage != null) {\n console.log('[translate] Usage:', {\n inputTokens: usage.inputTokens,\n cachedInputTokens: usage.cachedInputTokens,\n outputTokens: usage.outputTokens,\n totalTokens: usage.totalTokens,\n });\n if (estimate != null) {\n console.log(\n '[translate] Estimated cost (USD):',\n estimate.estimatedCostUsd.toFixed(8),\n );\n } else {\n console.log(`[translate] No pricing configured for model \"${model}\".`);\n }\n } else {\n console.log('[translate] Usage is unavailable');\n }\n\n assert(response.output_parsed != null, 'Response output parsed is null');\n\n return response.output_parsed;\n}\n"]}
@@ -0,0 +1 @@
1
+ import 'dotenv/config';
@@ -0,0 +1,139 @@
1
+ import 'dotenv/config';
2
+ import { resolve } from 'node:path';
3
+ import { describe } from 'vitest';
4
+ import { describeEval, StructuredOutputScorer } from 'vitest-evals';
5
+ import { FileStore } from '#repo/common/FileStore.js';
6
+ import { MRTDownRepository } from '#repo/MRTDownRepository.js';
7
+ import { triageNewEvidence, } from './index.js';
8
+ describe('triageNewEvidence', () => {
9
+ describeEval('should triage the new evidence into an existing issue or a new issue', {
10
+ // @ts-expect-error input is a string in the vitest-evals library
11
+ async data() {
12
+ const store = new FileStore(resolve(import.meta.dirname, '../../fixtures/data'));
13
+ const repo = new MRTDownRepository({ store });
14
+ return [
15
+ {
16
+ input: {
17
+ newEvidence: {
18
+ ts: '2026-01-01T07:10:00+08:00',
19
+ text: '[TGL] Due to a track fault at Tengah, train services on the Tengah Line are delayed between Bukit Batok and Bukit Merah Central',
20
+ },
21
+ repo,
22
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
23
+ toString() {
24
+ return '[DISRUPTION] Related to an existing issue';
25
+ },
26
+ },
27
+ expected: {
28
+ result: {
29
+ kind: 'part-of-existing-issue',
30
+ issueId: '2026-01-01-tgl-train-fault',
31
+ },
32
+ },
33
+ },
34
+ {
35
+ input: {
36
+ newEvidence: {
37
+ ts: '2026-01-01T07:10:00+08:00',
38
+ text: '[TGL] Due to a track fault at Tengah, train services on the Tengah Line are delayed between Bukit Merah Central and Outram Park',
39
+ },
40
+ repo,
41
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
42
+ toString() {
43
+ return '[DISRUPTION] Related to a new issue on a different part of the same line';
44
+ },
45
+ },
46
+ expected: {
47
+ result: {
48
+ kind: 'part-of-new-issue',
49
+ issueType: 'disruption',
50
+ },
51
+ },
52
+ },
53
+ {
54
+ input: {
55
+ newEvidence: {
56
+ ts: '2026-01-01T07:10:00+08:00',
57
+ text: '[TGL] Due to maintenance works, services on the Tengah Line will end earlier at 11pm tonight.',
58
+ },
59
+ repo,
60
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
61
+ toString() {
62
+ return '[MAINTENANCE] Related to a new issue';
63
+ },
64
+ },
65
+ expected: {
66
+ result: {
67
+ kind: 'part-of-new-issue',
68
+ issueType: 'maintenance',
69
+ },
70
+ },
71
+ },
72
+ {
73
+ input: {
74
+ newEvidence: {
75
+ ts: '2026-03-01T07:10:00+08:00',
76
+ text: '[SLL] Due to a track fault at Seletar, train services on the Seletar Line are delayed between Seletar Aerospace and Bukit Merah Central',
77
+ },
78
+ repo: new MRTDownRepository({
79
+ store: new FileStore(resolve(import.meta.dirname, '../fixtures/data')),
80
+ }),
81
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
82
+ toString() {
83
+ return '[DISRUPTION] Related to a new issue';
84
+ },
85
+ },
86
+ expected: {
87
+ result: {
88
+ kind: 'part-of-new-issue',
89
+ issueType: 'disruption',
90
+ },
91
+ },
92
+ },
93
+ {
94
+ input: {
95
+ newEvidence: {
96
+ ts: '2026-03-01T07:10:00+08:00',
97
+ text: '[SLL] MRT Platform screen doors at Seletar Line stations will undergo renewal works from 1st March to 31st March 2026.',
98
+ },
99
+ repo,
100
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
101
+ toString() {
102
+ return '[INFRA] Related to a new issue';
103
+ },
104
+ },
105
+ expected: {
106
+ result: {
107
+ kind: 'part-of-new-issue',
108
+ issueType: 'infra',
109
+ },
110
+ },
111
+ },
112
+ {
113
+ input: {
114
+ newEvidence: {
115
+ ts: '2026-03-01T07:10:00+08:00',
116
+ text: "Singapore's MRT system is the best in the world.",
117
+ },
118
+ repo,
119
+ // This is used by vitest-evals as the test name, as the library expects `input` to be a string.
120
+ toString() {
121
+ return 'Irrelevant content';
122
+ },
123
+ },
124
+ expected: {
125
+ result: {
126
+ kind: 'irrelevant-content',
127
+ },
128
+ },
129
+ },
130
+ ];
131
+ },
132
+ async task(input) {
133
+ const result = await triageNewEvidence(input);
134
+ return JSON.stringify(result);
135
+ },
136
+ scorers: [StructuredOutputScorer()],
137
+ });
138
+ });
139
+ //# sourceMappingURL=eval.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval.test.js","sourceRoot":"/","sources":["llm/functions/triageNewEvidence/eval.test.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAEvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAGL,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAEpB,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,YAAY,CACV,sEAAsE,EACtE;QACE,iEAAiE;QACjE,KAAK,CAAC,IAAI;YACR,MAAM,KAAK,GAAG,IAAI,SAAS,CACzB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,qBAAqB,CAAC,CACpD,CAAC;YACF,MAAM,IAAI,GAAG,IAAI,iBAAiB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAE9C,OAAO;gBACL;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,iIAAiI;yBACxI;wBACD,IAAI;wBACJ,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,2CAA2C,CAAC;wBACrD,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,wBAAwB;4BAC9B,OAAO,EAAE,4BAA4B;yBACtC;qBACF;iBACF;gBACD;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,iIAAiI;yBACxI;wBACD,IAAI;wBACJ,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,0EAA0E,CAAC;wBACpF,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,mBAAmB;4BACzB,SAAS,EAAE,YAAY;yBACxB;qBACF;iBACF;gBACD;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,+FAA+F;yBACtG;wBACD,IAAI;wBACJ,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,sCAAsC,CAAC;wBAChD,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,mBAAmB;4BACzB,SAAS,EAAE,aAAa;yBACzB;qBACF;iBACF;gBACD;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,yIAAyI;yBAChJ;wBACD,IAAI,EAAE,IAAI,iBAAiB,CAAC;4BAC1B,KAAK,EAAE,IAAI,SAAS,CAClB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAAC,CACjD;yBACF,CAAC;wBACF,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,qCAAqC,CAAC;wBAC/C,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,mBAAmB;4BACzB,SAAS,EAAE,YAAY;yBACxB;qBACF;iBACF;gBACD;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,wHAAwH;yBAC/H;wBACD,IAAI;wBACJ,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,gCAAgC,CAAC;wBAC1C,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,mBAAmB;4BACzB,SAAS,EAAE,OAAO;yBACnB;qBACF;iBACF;gBACD;oBACE,KAAK,EAAE;wBACL,WAAW,EAAE;4BACX,EAAE,EAAE,2BAA2B;4BAC/B,IAAI,EAAE,kDAAkD;yBACzD;wBACD,IAAI;wBACJ,gGAAgG;wBAChG,QAAQ;4BACN,OAAO,oBAAoB,CAAC;wBAC9B,CAAC;qBACF;oBACD,QAAQ,EAAE;wBACR,MAAM,EAAE;4BACN,IAAI,EAAE,oBAAoB;yBAC3B;qBACF;iBACF;aAIA,CAAC;QACN,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK;YACd,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC,KAA2C,CAC5C,CAAC;YACF,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,EAAE,CAAC,sBAAsB,EAAE,CAAC;KACpC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import 'dotenv/config';\n\nimport { resolve } from 'node:path';\nimport { describe } from 'vitest';\nimport { describeEval, StructuredOutputScorer } from 'vitest-evals';\nimport { FileStore } from '#repo/common/FileStore.js';\nimport { MRTDownRepository } from '#repo/MRTDownRepository.js';\nimport {\n type TriageNewEvidenceParams,\n type TriageNewEvidenceResult,\n triageNewEvidence,\n} from './index.js';\n\ndescribe('triageNewEvidence', () => {\n describeEval(\n 'should triage the new evidence into an existing issue or a new issue',\n {\n // @ts-expect-error input is a string in the vitest-evals library\n async data() {\n const store = new FileStore(\n resolve(import.meta.dirname, '../../fixtures/data'),\n );\n const repo = new MRTDownRepository({ store });\n\n return [\n {\n input: {\n newEvidence: {\n ts: '2026-01-01T07:10:00+08:00',\n text: '[TGL] Due to a track fault at Tengah, train services on the Tengah Line are delayed between Bukit Batok and Bukit Merah Central',\n },\n repo,\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return '[DISRUPTION] Related to an existing issue';\n },\n },\n expected: {\n result: {\n kind: 'part-of-existing-issue',\n issueId: '2026-01-01-tgl-train-fault',\n },\n },\n },\n {\n input: {\n newEvidence: {\n ts: '2026-01-01T07:10:00+08:00',\n text: '[TGL] Due to a track fault at Tengah, train services on the Tengah Line are delayed between Bukit Merah Central and Outram Park',\n },\n repo,\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return '[DISRUPTION] Related to a new issue on a different part of the same line';\n },\n },\n expected: {\n result: {\n kind: 'part-of-new-issue',\n issueType: 'disruption',\n },\n },\n },\n {\n input: {\n newEvidence: {\n ts: '2026-01-01T07:10:00+08:00',\n text: '[TGL] Due to maintenance works, services on the Tengah Line will end earlier at 11pm tonight.',\n },\n repo,\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return '[MAINTENANCE] Related to a new issue';\n },\n },\n expected: {\n result: {\n kind: 'part-of-new-issue',\n issueType: 'maintenance',\n },\n },\n },\n {\n input: {\n newEvidence: {\n ts: '2026-03-01T07:10:00+08:00',\n text: '[SLL] Due to a track fault at Seletar, train services on the Seletar Line are delayed between Seletar Aerospace and Bukit Merah Central',\n },\n repo: new MRTDownRepository({\n store: new FileStore(\n resolve(import.meta.dirname, '../fixtures/data'),\n ),\n }),\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return '[DISRUPTION] Related to a new issue';\n },\n },\n expected: {\n result: {\n kind: 'part-of-new-issue',\n issueType: 'disruption',\n },\n },\n },\n {\n input: {\n newEvidence: {\n ts: '2026-03-01T07:10:00+08:00',\n text: '[SLL] MRT Platform screen doors at Seletar Line stations will undergo renewal works from 1st March to 31st March 2026.',\n },\n repo,\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return '[INFRA] Related to a new issue';\n },\n },\n expected: {\n result: {\n kind: 'part-of-new-issue',\n issueType: 'infra',\n },\n },\n },\n {\n input: {\n newEvidence: {\n ts: '2026-03-01T07:10:00+08:00',\n text: \"Singapore's MRT system is the best in the world.\",\n },\n repo,\n // This is used by vitest-evals as the test name, as the library expects `input` to be a string.\n toString() {\n return 'Irrelevant content';\n },\n },\n expected: {\n result: {\n kind: 'irrelevant-content',\n },\n },\n },\n ] satisfies {\n input: TriageNewEvidenceParams & { toString(): string };\n expected: TriageNewEvidenceResult;\n }[];\n },\n async task(input) {\n const result = await triageNewEvidence(\n input as unknown as TriageNewEvidenceParams,\n );\n return JSON.stringify(result);\n },\n scorers: [StructuredOutputScorer()],\n },\n );\n});\n"]}
@@ -0,0 +1,37 @@
1
+ import z from 'zod';
2
+ import type { MRTDownRepository } from '#repo/MRTDownRepository.js';
3
+ declare const ResponseSchema: z.ZodObject<{
4
+ result: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
+ kind: z.ZodLiteral<"part-of-existing-issue">;
6
+ issueId: z.ZodString;
7
+ }, z.z.core.$strip>, z.ZodObject<{
8
+ kind: z.ZodLiteral<"part-of-new-issue">;
9
+ issueType: z.ZodEnum<{
10
+ disruption: "disruption";
11
+ maintenance: "maintenance";
12
+ infra: "infra";
13
+ }>;
14
+ }, z.z.core.$strip>, z.ZodObject<{
15
+ kind: z.ZodLiteral<"irrelevant-content">;
16
+ }, z.z.core.$strip>]>;
17
+ }, z.z.core.$strip>;
18
+ export type TriageNewEvidenceParams = {
19
+ newEvidence: {
20
+ ts: string;
21
+ text: string;
22
+ };
23
+ repo: MRTDownRepository;
24
+ };
25
+ export type TriageNewEvidenceResult = z.infer<typeof ResponseSchema>;
26
+ export declare function triageNewEvidence(params: TriageNewEvidenceParams): Promise<{
27
+ result: {
28
+ kind: "part-of-existing-issue";
29
+ issueId: string;
30
+ } | {
31
+ kind: "part-of-new-issue";
32
+ issueType: "disruption" | "maintenance" | "infra";
33
+ } | {
34
+ kind: "irrelevant-content";
35
+ };
36
+ }>;
37
+ export {};
@@ -0,0 +1,121 @@
1
+ import { DateTime } from 'luxon';
2
+ import z from 'zod';
3
+ import { openAiClient } from '#llm/client.js';
4
+ import { IssueIdSchema } from '#schema/issue/id.js';
5
+ import { IssueTypeSchema } from '#schema/issue/issueType.js';
6
+ import { assert } from '#util/assert.js';
7
+ import { buildSystemPrompt } from './prompt.js';
8
+ import { FindIssuesTool } from './tools/FindIssuesTool.js';
9
+ import { GetIssueTool } from './tools/GetIssueTool.js';
10
+ const TOOL_CALL_LIMIT = 5;
11
+ const ResponseSchema = z.object({
12
+ result: z.discriminatedUnion('type', [
13
+ z.object({
14
+ kind: z.literal('part-of-existing-issue'),
15
+ issueId: IssueIdSchema,
16
+ }),
17
+ z.object({
18
+ kind: z.literal('part-of-new-issue'),
19
+ issueType: IssueTypeSchema,
20
+ }),
21
+ z.object({
22
+ kind: z.literal('irrelevant-content'),
23
+ }),
24
+ ]),
25
+ });
26
+ export async function triageNewEvidence(params) {
27
+ const evidenceTs = DateTime.fromISO(params.newEvidence.ts);
28
+ assert(evidenceTs.isValid, `Invalid date: ${params.newEvidence.ts}`);
29
+ const findIssuesTool = new FindIssuesTool(params.repo);
30
+ const getIssueTool = new GetIssueTool(params.repo);
31
+ const toolRegistry = {
32
+ [findIssuesTool.name]: findIssuesTool,
33
+ [getIssueTool.name]: getIssueTool,
34
+ };
35
+ const systemPrompt = buildSystemPrompt();
36
+ const context = [
37
+ {
38
+ role: 'user',
39
+ content: `
40
+ Evidence: ${params.newEvidence.text}
41
+
42
+ Timestamp: ${evidenceTs.toISO({ includeOffset: true })}
43
+ `.trim(),
44
+ },
45
+ ];
46
+ let toolCallCount = 0;
47
+ let response;
48
+ do {
49
+ response = await openAiClient.responses.parse({
50
+ model: 'gpt-5-mini',
51
+ input: context,
52
+ instructions: systemPrompt,
53
+ text: {
54
+ format: {
55
+ type: 'json_schema',
56
+ name: 'Response',
57
+ strict: true,
58
+ schema: z.toJSONSchema(ResponseSchema),
59
+ },
60
+ },
61
+ tools: Object.values(toolRegistry).map((tool) => {
62
+ return {
63
+ type: 'function',
64
+ name: tool.name,
65
+ description: tool.description,
66
+ parameters: tool.paramsSchema,
67
+ strict: true,
68
+ };
69
+ }),
70
+ // Don't persist conversation with OpenAI, but include reasoning content to
71
+ // continue the thread with the same reasoning.
72
+ store: false,
73
+ include: ['reasoning.encrypted_content'],
74
+ });
75
+ for (const item of response.output) {
76
+ switch (item.type) {
77
+ case 'function_call': {
78
+ /**
79
+ * Prevent the `parsed_arguments` field from being included
80
+ * https://github.com/openai/openai-python/issues/2374
81
+ */
82
+ context.push({
83
+ type: 'function_call',
84
+ id: item.id,
85
+ call_id: item.call_id,
86
+ name: item.name,
87
+ arguments: item.arguments,
88
+ });
89
+ if (toolCallCount > TOOL_CALL_LIMIT) {
90
+ context.push({
91
+ type: 'function_call_output',
92
+ call_id: item.call_id,
93
+ output: 'Ran out of tool calls. Stop Calling.',
94
+ });
95
+ console.log('Forced short-circuit, returning error message in tool call result.');
96
+ }
97
+ if (item.name in toolRegistry) {
98
+ const tool = toolRegistry[item.name];
99
+ const params = tool.parseParams(JSON.parse(item.arguments));
100
+ // Call the tool's run function
101
+ const result = await tool.runner(params);
102
+ context.push({
103
+ type: 'function_call_output',
104
+ call_id: item.call_id,
105
+ output: result,
106
+ });
107
+ }
108
+ toolCallCount++;
109
+ break;
110
+ }
111
+ default: {
112
+ context.push(item);
113
+ break;
114
+ }
115
+ }
116
+ }
117
+ } while (response.output.some((item) => item.type === 'function_call'));
118
+ assert(response.output_parsed != null, 'Response output parsed is null');
119
+ return response.output_parsed;
120
+ }
121
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["llm/functions/triageNewEvidence/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport type {\n ParsedResponse,\n ResponseInputItem,\n} from 'openai/resources/responses/responses.mjs';\nimport z from 'zod';\nimport { openAiClient } from '#llm/client.js';\nimport type { MRTDownRepository } from '#repo/MRTDownRepository.js';\nimport { IssueIdSchema } from '#schema/issue/id.js';\nimport { IssueTypeSchema } from '#schema/issue/issueType.js';\nimport { assert } from '#util/assert.js';\nimport type { ToolRegistry } from '../../common/tool.js';\nimport { buildSystemPrompt } from './prompt.js';\nimport { FindIssuesTool } from './tools/FindIssuesTool.js';\nimport { GetIssueTool } from './tools/GetIssueTool.js';\n\nconst TOOL_CALL_LIMIT = 5;\n\nconst ResponseSchema = z.object({\n result: z.discriminatedUnion('type', [\n z.object({\n kind: z.literal('part-of-existing-issue'),\n issueId: IssueIdSchema,\n }),\n z.object({\n kind: z.literal('part-of-new-issue'),\n issueType: IssueTypeSchema,\n }),\n z.object({\n kind: z.literal('irrelevant-content'),\n }),\n ]),\n});\n\nexport type TriageNewEvidenceParams = {\n newEvidence: {\n ts: string;\n text: string;\n };\n repo: MRTDownRepository;\n};\n\nexport type TriageNewEvidenceResult = z.infer<typeof ResponseSchema>;\n\nexport async function triageNewEvidence(params: TriageNewEvidenceParams) {\n const evidenceTs = DateTime.fromISO(params.newEvidence.ts);\n assert(evidenceTs.isValid, `Invalid date: ${params.newEvidence.ts}`);\n\n const findIssuesTool = new FindIssuesTool(params.repo);\n const getIssueTool = new GetIssueTool(params.repo);\n const toolRegistry: ToolRegistry = {\n [findIssuesTool.name]: findIssuesTool,\n [getIssueTool.name]: getIssueTool,\n };\n\n const systemPrompt = buildSystemPrompt();\n\n const context: ResponseInputItem[] = [\n {\n role: 'user',\n content: `\nEvidence: ${params.newEvidence.text}\n\nTimestamp: ${evidenceTs.toISO({ includeOffset: true })}\n`.trim(),\n },\n ];\n\n let toolCallCount = 0;\n\n let response: ParsedResponse<z.infer<typeof ResponseSchema>>;\n do {\n response = await openAiClient.responses.parse({\n model: 'gpt-5-mini',\n input: context,\n instructions: systemPrompt,\n text: {\n format: {\n type: 'json_schema',\n name: 'Response',\n strict: true,\n schema: z.toJSONSchema(ResponseSchema),\n },\n },\n tools: Object.values(toolRegistry).map((tool) => {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: tool.paramsSchema,\n strict: true,\n };\n }),\n // Don't persist conversation with OpenAI, but include reasoning content to\n // continue the thread with the same reasoning.\n store: false,\n include: ['reasoning.encrypted_content'],\n });\n\n for (const item of response.output) {\n switch (item.type) {\n case 'function_call': {\n /**\n * Prevent the `parsed_arguments` field from being included\n * https://github.com/openai/openai-python/issues/2374\n */\n context.push({\n type: 'function_call',\n id: item.id,\n call_id: item.call_id,\n name: item.name,\n arguments: item.arguments,\n });\n\n if (toolCallCount > TOOL_CALL_LIMIT) {\n context.push({\n type: 'function_call_output',\n call_id: item.call_id,\n output: 'Ran out of tool calls. Stop Calling.',\n });\n console.log(\n 'Forced short-circuit, returning error message in tool call result.',\n );\n }\n\n if (item.name in toolRegistry) {\n const tool = toolRegistry[item.name];\n\n const params = tool.parseParams(JSON.parse(item.arguments));\n // Call the tool's run function\n const result = await tool.runner(params);\n\n context.push({\n type: 'function_call_output',\n call_id: item.call_id,\n output: result,\n });\n }\n\n toolCallCount++;\n break;\n }\n default: {\n context.push(item);\n break;\n }\n }\n }\n } while (response.output.some((item) => item.type === 'function_call'));\n\n assert(response.output_parsed != null, 'Response output parsed is null');\n\n return response.output_parsed;\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function buildSystemPrompt(): string;
@@ -0,0 +1,60 @@
1
+ export function buildSystemPrompt() {
2
+ return `
3
+ You are an expert at triaging new evidence into an existing issue or a new issue.
4
+
5
+ Your task: Triage the new evidence into an existing issue or a new issue.
6
+
7
+ ISSUE TYPES:
8
+ - disruption: Service disruptions (e.g. train delays, line faults, operational failures)
9
+ - maintenance: Planned maintenance works (e.g. system upgrades, infrastructure maintenance)
10
+ - infra: Infrastructure issues (e.g. station lift outages, platform door faults, facility breakdowns)
11
+
12
+ DECISION PROCESS:
13
+ 1. Extract key information from evidence: affected service/line, location, issue type, time window
14
+ 2. Use findIssues tool to search for related issues by service name or line
15
+ 3. Use getIssue tool to review each candidate issue's scope, periods, and effects
16
+ 4. Compare evidence location and timing with existing issue scope
17
+ 5. Return appropriate classification with clear reasoning
18
+
19
+ CLASSIFICATION RULES:
20
+
21
+ Part of Existing Issue:
22
+ - Evidence must match the service/line AND have geographic overlap (stations or segments)
23
+ - Temporal proximity matters: evidence should occur during or immediately adjacent to the issue's period
24
+ - Multiple separate incidents on the same service are separate issues (not continuous scope expansion)
25
+
26
+ Part of New Issue:
27
+ - Evidence describes a distinct incident not covered by existing issues
28
+ - Different geographic location on the same service (e.g. different stations/segments)
29
+ - Different service/line altogether
30
+ - Different time period or issue type
31
+
32
+ Irrelevant Content:
33
+ - Opinion or commentary without operational details
34
+ - General statements without specific service/location/time information
35
+ - Marketing or non-operational content
36
+
37
+ SPECIFIC GUIDANCE BY ISSUE TYPE:
38
+
39
+ DISRUPTIONS:
40
+ - Location specificity is CRITICAL - different stations or segments are separate incidents
41
+ - Same service line alone is NOT sufficient for matching
42
+ - Must have EXACT geographic overlap: if evidence mentions different station pair, it's a new issue
43
+ - Examples: "fault between A and B" overlaps with existing issue only if existing covers A-B segment
44
+ - A fault "between B and C" on the same line is a different incident, even if it shares one endpoint station
45
+
46
+ MAINTENANCE:
47
+ - Service-level planned works that affect operating hours or service availability
48
+ - Examples: early line closures, reduced service windows, system upgrades affecting all trains
49
+ - NOT about specific facility repairs (those are infra)
50
+
51
+ INFRASTRUCTURE:
52
+ - Specific facility or asset breakdowns that need repair or renewal
53
+ - Examples: lift outages, platform screen door faults/renewal, escalator repairs, door malfunctions
54
+ - Facility-specific: affects particular station and facility type (e.g. lift at Station X)
55
+ - Can be scheduled (renewal works) or unplanned (breakdowns)
56
+ - Link to existing issue if same station, same facility, service still active
57
+ - Different station or facility type = new issue
58
+ `.trim();
59
+ }
60
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"/","sources":["llm/functions/triageNewEvidence/prompt.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwDR,CAAC,IAAI,EAAE,CAAC;AACT,CAAC","sourcesContent":["export function buildSystemPrompt() {\n return `\nYou are an expert at triaging new evidence into an existing issue or a new issue.\n\nYour task: Triage the new evidence into an existing issue or a new issue.\n\nISSUE TYPES:\n- disruption: Service disruptions (e.g. train delays, line faults, operational failures)\n- maintenance: Planned maintenance works (e.g. system upgrades, infrastructure maintenance)\n- infra: Infrastructure issues (e.g. station lift outages, platform door faults, facility breakdowns)\n\nDECISION PROCESS:\n1. Extract key information from evidence: affected service/line, location, issue type, time window\n2. Use findIssues tool to search for related issues by service name or line\n3. Use getIssue tool to review each candidate issue's scope, periods, and effects\n4. Compare evidence location and timing with existing issue scope\n5. Return appropriate classification with clear reasoning\n\nCLASSIFICATION RULES:\n\nPart of Existing Issue:\n- Evidence must match the service/line AND have geographic overlap (stations or segments)\n- Temporal proximity matters: evidence should occur during or immediately adjacent to the issue's period\n- Multiple separate incidents on the same service are separate issues (not continuous scope expansion)\n\nPart of New Issue:\n- Evidence describes a distinct incident not covered by existing issues\n- Different geographic location on the same service (e.g. different stations/segments)\n- Different service/line altogether\n- Different time period or issue type\n\nIrrelevant Content:\n- Opinion or commentary without operational details\n- General statements without specific service/location/time information\n- Marketing or non-operational content\n\nSPECIFIC GUIDANCE BY ISSUE TYPE:\n\nDISRUPTIONS:\n- Location specificity is CRITICAL - different stations or segments are separate incidents\n- Same service line alone is NOT sufficient for matching\n- Must have EXACT geographic overlap: if evidence mentions different station pair, it's a new issue\n- Examples: \"fault between A and B\" overlaps with existing issue only if existing covers A-B segment\n- A fault \"between B and C\" on the same line is a different incident, even if it shares one endpoint station\n\nMAINTENANCE:\n- Service-level planned works that affect operating hours or service availability\n- Examples: early line closures, reduced service windows, system upgrades affecting all trains\n- NOT about specific facility repairs (those are infra)\n\nINFRASTRUCTURE:\n- Specific facility or asset breakdowns that need repair or renewal\n- Examples: lift outages, platform screen door faults/renewal, escalator repairs, door malfunctions\n- Facility-specific: affects particular station and facility type (e.g. lift at Station X)\n- Can be scheduled (renewal works) or unplanned (breakdowns)\n- Link to existing issue if same station, same facility, service still active\n- Different station or facility type = new issue\n`.trim();\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import z from 'zod';
2
+ import { Tool } from '#llm/common/tool.js';
3
+ import type { MRTDownRepository } from '#repo/MRTDownRepository.js';
4
+ declare const FindIssuesToolParametersSchema: z.ZodObject<{
5
+ query: z.ZodString;
6
+ }, z.z.core.$strip>;
7
+ type FindIssuesToolParameters = z.infer<typeof FindIssuesToolParametersSchema>;
8
+ export declare class FindIssuesTool extends Tool<FindIssuesToolParameters> {
9
+ name: string;
10
+ description: string;
11
+ private readonly repo;
12
+ constructor(repo: MRTDownRepository);
13
+ get paramsSchema(): {
14
+ [key: string]: unknown;
15
+ };
16
+ parseParams(params: unknown): FindIssuesToolParameters;
17
+ runner(params: FindIssuesToolParameters): Promise<string>;
18
+ }
19
+ export {};