@mrtdown/core 2.0.0-alpha.2 → 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 (112) 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/index.d.ts +20 -11
  17. package/dist/index.js +20 -11
  18. package/dist/index.js.map +1 -1
  19. package/dist/llm/client.d.ts +2 -0
  20. package/dist/llm/client.js +5 -0
  21. package/dist/llm/client.js.map +1 -0
  22. package/dist/llm/common/MemoryStore.d.ts +21 -0
  23. package/dist/llm/common/MemoryStore.js +100 -0
  24. package/dist/llm/common/MemoryStore.js.map +1 -0
  25. package/dist/llm/common/MemoryStore.test.d.ts +1 -0
  26. package/dist/llm/common/MemoryStore.test.js +225 -0
  27. package/dist/llm/common/MemoryStore.test.js.map +1 -0
  28. package/dist/llm/common/formatCurrentState.d.ts +10 -0
  29. package/dist/llm/common/formatCurrentState.js +342 -0
  30. package/dist/llm/common/formatCurrentState.js.map +1 -0
  31. package/dist/llm/common/tool.d.ts +32 -0
  32. package/dist/llm/common/tool.js +6 -0
  33. package/dist/llm/common/tool.js.map +1 -0
  34. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.d.ts +1 -0
  35. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js +433 -0
  36. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js.map +1 -0
  37. package/dist/llm/functions/extractClaimsFromNewEvidence/index.d.ts +18 -0
  38. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js +153 -0
  39. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js.map +1 -0
  40. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.d.ts +1 -0
  41. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js +168 -0
  42. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js.map +1 -0
  43. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.d.ts +19 -0
  44. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js +65 -0
  45. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js.map +1 -0
  46. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.d.ts +21 -0
  47. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js +115 -0
  48. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js.map +1 -0
  49. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.d.ts +24 -0
  50. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js +110 -0
  51. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js.map +1 -0
  52. package/dist/llm/functions/generateIssueTitleAndSlug/index.d.ts +14 -0
  53. package/dist/llm/functions/generateIssueTitleAndSlug/index.js +38 -0
  54. package/dist/llm/functions/generateIssueTitleAndSlug/index.js.map +1 -0
  55. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.d.ts +1 -0
  56. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js +23 -0
  57. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js.map +1 -0
  58. package/dist/llm/functions/translate/index.d.ts +1 -0
  59. package/dist/llm/functions/translate/index.js +59 -0
  60. package/dist/llm/functions/translate/index.js.map +1 -0
  61. package/dist/llm/functions/triageNewEvidence/eval.test.d.ts +1 -0
  62. package/dist/llm/functions/triageNewEvidence/eval.test.js +139 -0
  63. package/dist/llm/functions/triageNewEvidence/eval.test.js.map +1 -0
  64. package/dist/llm/functions/triageNewEvidence/index.d.ts +37 -0
  65. package/dist/llm/functions/triageNewEvidence/index.js +121 -0
  66. package/dist/llm/functions/triageNewEvidence/index.js.map +1 -0
  67. package/dist/llm/functions/triageNewEvidence/prompt.d.ts +1 -0
  68. package/dist/llm/functions/triageNewEvidence/prompt.js +60 -0
  69. package/dist/llm/functions/triageNewEvidence/prompt.js.map +1 -0
  70. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.d.ts +19 -0
  71. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js +65 -0
  72. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js.map +1 -0
  73. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.d.ts +19 -0
  74. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js +37 -0
  75. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js.map +1 -0
  76. package/dist/scripts/ingestViaWebhook.d.ts +1 -0
  77. package/dist/scripts/ingestViaWebhook.js +9 -0
  78. package/dist/scripts/ingestViaWebhook.js.map +1 -0
  79. package/dist/validators/buildContext.d.ts +7 -0
  80. package/dist/validators/buildContext.js +164 -0
  81. package/dist/validators/buildContext.js.map +1 -0
  82. package/dist/validators/index.d.ts +17 -0
  83. package/dist/validators/index.js +58 -0
  84. package/dist/validators/index.js.map +1 -0
  85. package/dist/validators/issue.d.ts +13 -0
  86. package/dist/validators/issue.js +220 -0
  87. package/dist/validators/issue.js.map +1 -0
  88. package/dist/validators/landmark.d.ts +7 -0
  89. package/dist/validators/landmark.js +43 -0
  90. package/dist/validators/landmark.js.map +1 -0
  91. package/dist/validators/line.d.ts +8 -0
  92. package/dist/validators/line.js +87 -0
  93. package/dist/validators/line.js.map +1 -0
  94. package/dist/validators/operator.d.ts +7 -0
  95. package/dist/validators/operator.js +43 -0
  96. package/dist/validators/operator.js.map +1 -0
  97. package/dist/validators/service.d.ts +8 -0
  98. package/dist/validators/service.js +87 -0
  99. package/dist/validators/service.js.map +1 -0
  100. package/dist/validators/station.d.ts +8 -0
  101. package/dist/validators/station.js +93 -0
  102. package/dist/validators/station.js.map +1 -0
  103. package/dist/validators/town.d.ts +7 -0
  104. package/dist/validators/town.js +43 -0
  105. package/dist/validators/town.js.map +1 -0
  106. package/dist/validators/types.d.ts +19 -0
  107. package/dist/validators/types.js +2 -0
  108. package/dist/validators/types.js.map +1 -0
  109. package/dist/validators/utils.d.ts +2 -0
  110. package/dist/validators/utils.js +9 -0
  111. package/dist/validators/utils.js.map +1 -0
  112. package/package.json +11 -7
@@ -0,0 +1,153 @@
1
+ import { DateTime } from 'luxon';
2
+ import z from 'zod';
3
+ import { estimateOpenAICostFromUsage, normalizeOpenAIResponsesUsage, } from '#helpers/estimateOpenAICost.js';
4
+ import { openAiClient } from '#llm/client.js';
5
+ import { ClaimSchema } from '#schema/issue/claim.js';
6
+ import { assert } from '#util/assert.js';
7
+ import { buildSystemPrompt } from './prompt.js';
8
+ import { FindLinesTool } from './tools/FindLinesTool.js';
9
+ import { FindServicesTool } from './tools/FindServicesTool.js';
10
+ import { FindStationsTool } from './tools/FindStationsTool.js';
11
+ const TOOL_CALL_LIMIT = 5;
12
+ const ResponseSchema = z.object({
13
+ claims: z.array(ClaimSchema),
14
+ });
15
+ /**
16
+ * Extract claims from new evidence.
17
+ * @param params
18
+ * @returns
19
+ */
20
+ export async function extractClaimsFromNewEvidence(params) {
21
+ const evidenceTs = DateTime.fromISO(params.newEvidence.ts);
22
+ assert(evidenceTs.isValid, `Invalid date: ${params.newEvidence.ts}`);
23
+ const findStationsTool = new FindStationsTool(evidenceTs, params.repo);
24
+ const findServicesTool = new FindServicesTool(evidenceTs, params.repo);
25
+ const findLinesTool = new FindLinesTool(params.repo);
26
+ const toolRegistry = {
27
+ [findStationsTool.name]: findStationsTool,
28
+ [findServicesTool.name]: findServicesTool,
29
+ [findLinesTool.name]: findLinesTool,
30
+ };
31
+ const context = [
32
+ {
33
+ role: 'user',
34
+ content: `
35
+ Evidence: ${params.newEvidence.text}
36
+
37
+ Timestamp: ${evidenceTs.toISO({ includeOffset: true })}
38
+ `.trim(),
39
+ },
40
+ ];
41
+ const systemPrompt = buildSystemPrompt();
42
+ const model = 'gpt-5-mini';
43
+ let toolCallCount = 0;
44
+ let response;
45
+ do {
46
+ response = await openAiClient.responses.parse({
47
+ model,
48
+ instructions: systemPrompt,
49
+ input: context,
50
+ reasoning: {
51
+ effort: 'medium',
52
+ summary: 'concise',
53
+ },
54
+ text: {
55
+ format: {
56
+ type: 'json_schema',
57
+ name: 'Response',
58
+ strict: true,
59
+ schema: z.toJSONSchema(ResponseSchema),
60
+ },
61
+ },
62
+ tools: Object.values(toolRegistry).map((tool) => {
63
+ return {
64
+ type: 'function',
65
+ name: tool.name,
66
+ description: tool.description,
67
+ parameters: tool.paramsSchema,
68
+ strict: true,
69
+ };
70
+ }),
71
+ // Don't persist conversation with OpenAI, but include reasoning content to
72
+ // continue the thread with the same reasoning.
73
+ store: false,
74
+ include: ['reasoning.encrypted_content'],
75
+ });
76
+ for (const item of response.output) {
77
+ switch (item.type) {
78
+ case 'function_call': {
79
+ /**
80
+ * Prevent the `parsed_arguments` field from being included
81
+ * https://github.com/openai/openai-python/issues/2374
82
+ */
83
+ context.push({
84
+ type: 'function_call',
85
+ id: item.id,
86
+ call_id: item.call_id,
87
+ name: item.name,
88
+ arguments: item.arguments,
89
+ });
90
+ if (toolCallCount > TOOL_CALL_LIMIT) {
91
+ context.push({
92
+ type: 'function_call_output',
93
+ call_id: item.call_id,
94
+ output: 'Ran out of tool calls. Stop Calling.',
95
+ });
96
+ console.log('Forced short-circuit, returning error message in tool call result.');
97
+ }
98
+ if (item.name in toolRegistry) {
99
+ const tool = toolRegistry[item.name];
100
+ let params;
101
+ try {
102
+ params = tool.parseParams(JSON.parse(item.arguments));
103
+ }
104
+ catch (e) {
105
+ console.error(`[extractClaimsFromNewEvidence] Error parsing parameters for tool "${item.name}" with arguments "${item.arguments}":`, e);
106
+ context.push({
107
+ type: 'function_call_output',
108
+ call_id: item.call_id,
109
+ output: `Invalid parameters for tool "${item.name}". Please try again.`,
110
+ });
111
+ continue;
112
+ }
113
+ // Call the tool's run function
114
+ const result = await tool.runner(params);
115
+ context.push({
116
+ type: 'function_call_output',
117
+ call_id: item.call_id,
118
+ output: result,
119
+ });
120
+ }
121
+ toolCallCount++;
122
+ break;
123
+ }
124
+ default: {
125
+ context.push(item);
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ const usage = normalizeOpenAIResponsesUsage(response.usage);
131
+ const estimate = estimateOpenAICostFromUsage({ model, usage });
132
+ if (usage != null) {
133
+ console.log('[extractClaimsFromNewEvidence] Usage:', {
134
+ inputTokens: usage.inputTokens,
135
+ cachedInputTokens: usage.cachedInputTokens,
136
+ outputTokens: usage.outputTokens,
137
+ totalTokens: usage.totalTokens,
138
+ });
139
+ if (estimate != null) {
140
+ console.log('[extractClaimsFromNewEvidence] Estimated cost (USD):', estimate.estimatedCostUsd.toFixed(8));
141
+ }
142
+ else {
143
+ console.log(`[extractClaimsFromNewEvidence] No pricing configured for model "${model}".`);
144
+ }
145
+ }
146
+ else {
147
+ console.log('[extractClaimsFromNewEvidence] Usage is unavailable');
148
+ }
149
+ } while (response.output.some((item) => item.type === 'function_call'));
150
+ assert(response.output_parsed != null, 'Response output parsed is null');
151
+ return response.output_parsed;
152
+ }
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["llm/functions/extractClaimsFromNewEvidence/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.js';\nimport z from 'zod';\nimport {\n estimateOpenAICostFromUsage,\n normalizeOpenAIResponsesUsage,\n} from '#helpers/estimateOpenAICost.js';\nimport { openAiClient } from '#llm/client.js';\nimport type { MRTDownRepository } from '#repo/MRTDownRepository.js';\nimport { type Claim, ClaimSchema } from '#schema/issue/claim.js';\nimport { assert } from '#util/assert.js';\nimport type { ToolRegistry } from '../../common/tool.js';\nimport { buildSystemPrompt } from './prompt.js';\nimport { FindLinesTool } from './tools/FindLinesTool.js';\nimport { FindServicesTool } from './tools/FindServicesTool.js';\nimport { FindStationsTool } from './tools/FindStationsTool.js';\n\nconst TOOL_CALL_LIMIT = 5;\n\nconst ResponseSchema = z.object({\n claims: z.array(ClaimSchema),\n});\n\nexport interface ExtractClaimsFromNewEvidenceParams {\n newEvidence: {\n ts: string;\n text: string;\n };\n repo: MRTDownRepository;\n}\n\nexport type ExtractClaimsFromNewEvidenceResult = {\n claims: Claim[];\n};\n\n/**\n * Extract claims from new evidence.\n * @param params\n * @returns\n */\nexport async function extractClaimsFromNewEvidence(\n params: ExtractClaimsFromNewEvidenceParams,\n): Promise<ExtractClaimsFromNewEvidenceResult> {\n const evidenceTs = DateTime.fromISO(params.newEvidence.ts);\n assert(evidenceTs.isValid, `Invalid date: ${params.newEvidence.ts}`);\n\n const findStationsTool = new FindStationsTool(evidenceTs, params.repo);\n const findServicesTool = new FindServicesTool(evidenceTs, params.repo);\n const findLinesTool = new FindLinesTool(params.repo);\n const toolRegistry: ToolRegistry = {\n [findStationsTool.name]: findStationsTool,\n [findServicesTool.name]: findServicesTool,\n [findLinesTool.name]: findLinesTool,\n };\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 const systemPrompt = buildSystemPrompt();\n const model = 'gpt-5-mini';\n\n let toolCallCount = 0;\n\n let response: ParsedResponse<z.infer<typeof ResponseSchema>>;\n do {\n response = await openAiClient.responses.parse({\n model,\n instructions: systemPrompt,\n input: context,\n reasoning: {\n effort: 'medium',\n summary: 'concise',\n },\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\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 let params: unknown;\n\n try {\n params = tool.parseParams(JSON.parse(item.arguments));\n } catch (e) {\n console.error(\n `[extractClaimsFromNewEvidence] Error parsing parameters for tool \"${item.name}\" with arguments \"${item.arguments}\":`,\n e,\n );\n context.push({\n type: 'function_call_output',\n call_id: item.call_id,\n output: `Invalid parameters for tool \"${item.name}\". Please try again.`,\n });\n continue;\n }\n\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\n const usage = normalizeOpenAIResponsesUsage(response.usage);\n const estimate = estimateOpenAICostFromUsage({ model, usage });\n if (usage != null) {\n console.log('[extractClaimsFromNewEvidence] 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 '[extractClaimsFromNewEvidence] Estimated cost (USD):',\n estimate.estimatedCostUsd.toFixed(8),\n );\n } else {\n console.log(\n `[extractClaimsFromNewEvidence] No pricing configured for model \"${model}\".`,\n );\n }\n } else {\n console.log('[extractClaimsFromNewEvidence] Usage is unavailable');\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,168 @@
1
+ export function buildSystemPrompt() {
2
+ return `
3
+ You are an expert assistant that extracts structured impact claims from MRT evidence.
4
+
5
+ Return JSON that matches the provided schema exactly.
6
+
7
+ ## Goal
8
+ Given:
9
+ - Evidence text
10
+ - Evidence timestamp
11
+
12
+ Extract only operational impact claims (service or station facility impact) that are explicitly stated or strongly implied by the evidence.
13
+
14
+ ## Available tools
15
+ - findLines(lineNames): use to resolve line IDs from line names.
16
+ - findServices(lineId): use to resolve service IDs and service path stations.
17
+ - findStations(stationNames): use to resolve station IDs from station names/codes.
18
+
19
+ Use tools whenever an ID or station mapping is uncertain. Prefer correctness over guessing.
20
+
21
+ Important: Line ID is not service ID. A line can have multiple services. For service claims, always use serviceId from findServices; never use lineId from findLines.
22
+
23
+ Search for valid IDs; never invent or fabricate service IDs, line IDs, or station IDs. When searching for lines or services, try several variations (e.g. "NSL", "NS Line", "North-South Line") as special characters and formatting can be sensitive.
24
+
25
+ ## Claim construction rules
26
+ - One claim per affected entity.
27
+ - Use stable IDs returned by tools.
28
+ - If evidence is non-operational noise (links, generic advisory, no new impact fact), return no claims.
29
+
30
+ Irrelevance gate (strict):
31
+ - Return claims: [] unless the evidence contains at least one concrete operational assertion about impact state (e.g. delay, no-service, reduced-service, service-hours-adjustment, facility outage/degradation, clear/resume, or explicit planned impact window).
32
+ - Do not generate claims from tags/headers alone (e.g. "[LINE]", "UPDATE", hashtags) without an operational assertion.
33
+ - Advisory-only content (alternative routes, travel advice, support links, "refer to" links, reminders) is irrelevant unless it also includes a concrete impact update.
34
+ - If unsure whether new operational state is stated, prefer no claims.
35
+
36
+ ### entity
37
+ - For train line/service disruptions or maintenance, use:
38
+ - { type: "service", serviceId }
39
+ - serviceId must come from findServices (not lineId from findLines). Search for a valid service via findServices; do not make up a service ID.
40
+ - For station facility faults (lift/escalator/screen-door), use:
41
+ - { type: "facility", stationId, kind }
42
+
43
+ ### statusSignal
44
+ - "open": active disruption/degradation now.
45
+ - "cleared": evidence says issue resolved/resumed/normal service restored.
46
+ - "planned": future scheduled maintenance/planned adjustment.
47
+
48
+ ### effect
49
+ Use object shape with both keys: { service, facility }.
50
+
51
+ For service entities:
52
+ - Delayed service -> service: { kind: "delay", duration: null unless exact ISO duration is known }
53
+ - Service suspended/closed/no trains -> service: { kind: "no-service" }
54
+ - Reduced frequency/capacity -> service: { kind: "reduced-service" }
55
+ - Temporary operating-hour changes (start later/end earlier/shortened hours) -> service: { kind: "service-hours-adjustment" }
56
+ - If cleared/restored and no ongoing impact remains -> service: null
57
+ - facility must be null
58
+
59
+ For facility entities:
60
+ - Facility unavailable -> facility: { kind: "facility-out-of-service" }
61
+ - Facility degraded/partially available -> facility: { kind: "facility-degraded" }
62
+ - service must be null
63
+
64
+ ### scopes.service
65
+ - For full line/service statements ("train service resumed", "line closed"), use:
66
+ - [{ type: "service.whole" }]
67
+ - For "between A and B", use service.segment with station IDs:
68
+ - { type: "service.segment", fromStationId, toStationId }
69
+ - For station-specific service impact at one station, use:
70
+ - { type: "service.point", stationId }
71
+ - For service entities, scopes.service should generally be non-null.
72
+ - service.segment is directional: fromStationId -> toStationId is an ordered path, not an unordered pair.
73
+ - Validate segment orientation against the specific service path returned by findServices.
74
+ - For bidirectional output, create one claim per directional service and set segment endpoints to match each service direction (reverse endpoints for reverse direction service).
75
+ - Do not copy the same from/to ordering into both directional services unless that ordering is valid for both service paths.
76
+
77
+ Direction handling:
78
+ - Priority rule: if direction is ambiguous, default to bidirectional. This rule overrides heuristics.
79
+ - Assume service impact is bidirectional unless direction is stated with strong, unambiguous directional wording.
80
+ - Treat directional impact as valid only when phrasing is truly explicit, for example: "from X to Y towards Z", "towards Z only", "eastbound only", "westbound only", or equivalent unambiguous one-direction wording.
81
+ - "from X to Y" by itself is segment geometry, not directional exclusivity. Do not infer single-direction impact from endpoints alone.
82
+ - Mentions of one segment pair (or one station pair) do not imply one-direction impact by themselves.
83
+ - When direction is not explicitly limited, emit claims for each directional service on the affected line/service.
84
+ - For segment scopes, preserve path direction per service (reverse endpoints for reverse direction service).
85
+
86
+ ### timeHints
87
+ Best effort from evidence. Use null when unknown.
88
+
89
+ timeHints shape:
90
+ - timeHints is either null or exactly one of:
91
+ - { kind: "fixed", startAt, endAt }
92
+ - { kind: "recurring", frequency, startAt, endAt, daysOfWeek, timeWindow, timeZone, excludedDates }
93
+ - { kind: "start-only", startAt }
94
+ - { kind: "end-only", endAt }
95
+ - Do not use the old nested shape with startAt/endAt/recurring fields.
96
+
97
+ Maintenance period rule:
98
+ - For maintenance/planned adjustments, timeHints.startAt/timeHints.endAt must represent the overall maintenance window (calendar period), not the daily active service-impact hours.
99
+ - Put repeated daily/weekly impact hours into recurring.timeWindow.
100
+ - For service-hours-adjustment, recurring.timeWindow must represent when impact applies (restricted/no service), NOT normal operating hours.
101
+ - Example: if temporary operating hours are "first train at 05:45, last train at 23:15", then service runs during 05:45-23:15 and impact interval is 23:15-05:45; set recurring.timeWindow to { startAt: "23:15:00", endAt: "05:45:00" }.
102
+ - Never encode the service-running window as recurring.timeWindow for service-hours-adjustment.
103
+
104
+ When choosing kind:
105
+ - Use "fixed" when there is a bounded interval (start and maybe end).
106
+ - Use "recurring" when there is a repeating pattern.
107
+ - Use "start-only" when only a start signal is known.
108
+ - Use "end-only" when only an end/clear signal is known.
109
+
110
+ Field guidance:
111
+ - startAt:
112
+ - For newly active disruptions, default to evidence timestamp unless a different start time is stated.
113
+ - For planned items, use the planned start if stated.
114
+ - For "service will start at X" or "first train at X": the impact window (no service) is from start of day (00:00) until X. Use kind "fixed" with startAt at midnight and endAt at the stated start time.
115
+ - endAt:
116
+ - For cleared claims with no start in this evidence, prefer kind "end-only" and set endAt to evidence timestamp.
117
+ - For planned windows, use stated/plausible period end when explicit.
118
+ - For kind "fixed" and kind "end-only", endAt is exclusive.
119
+ - recurring:
120
+ - Populate recurring only for repeating patterns.
121
+ - recurring.startAt and recurring.endAt map to RRULE DTSTART and UNTIL.
122
+ - RRULE UNTIL semantics: recurring.endAt is the last occurrence anchor (inclusive), not the day after.
123
+ - Do not default recurring.startAt/endAt to 00:00:00 unless evidence explicitly indicates all-day or midnight boundaries.
124
+ - Preserve meaningful time-of-day from the described impact window.
125
+ - The anchor-time rule below is specific to maintenance/infra recurring timeHints:
126
+ - recurring.startAt/endAt must anchor to the impact START instant (same clock time as recurring.timeWindow.startAt), not recurring.timeWindow.endAt.
127
+ - For overnight windows, keep start-side anchors across the recurrence range.
128
+ - Example (non-test): if recurring.timeWindow is { startAt: "22:30:00", endAt: "05:00:00" } on weekdays from 10 Mar to 21 Mar, use startAt/endAt with 22:30 anchors on the first/last applicable dates, not 05:00 anchors.
129
+ - Shape:
130
+ - kind: "recurring"
131
+ - frequency: daily | weekly | monthly | yearly
132
+ - startAt, endAt as ISO8601 datetimes with offset
133
+ - daysOfWeek: [MO..SU] or null
134
+ - timeZone: "Asia/Singapore"
135
+ - timeWindow: { startAt: "HH:MM:SS", endAt: "HH:MM:SS" }
136
+ - excludedDates: null unless explicitly stated
137
+
138
+ ## Precision and normalization
139
+ - Prefer explicit facts; avoid inventing causes/details.
140
+ - Use canonical station/service IDs from tools.
141
+ - If only line name is given, map to all relevant services on that line.
142
+ - For ISO8601 datetimes, include timezone offset and seconds. Omit fractional seconds when milliseconds are zero (e.g. use 2026-01-01T07:10:00+08:00, not 2026-01-01T07:10:00.000+08:00).
143
+ - Treat fixed/end-only endAt as exclusive, and recurring.timeWindow.endAt as the end boundary of each daily window.
144
+ - Midnight timestamps are allowed only when explicitly justified by evidence (e.g. "from 00:00", "until midnight", or clear date-only all-day semantics).
145
+ - Keep claims minimal but complete for downstream state updates.
146
+ - Final self-check before returning:
147
+ - If evidence has no unambiguous one-direction qualifier, ensure claims include all directional services for the affected service/line.
148
+ - Only return a single directional service claim when explicit direction-only wording is present.
149
+ - Do not include commentary, only schema-conforming JSON.
150
+
151
+ ### causes
152
+ - causes must always be present on every claim.
153
+ - Use null when the evidence does not state or strongly imply a concrete cause subtype.
154
+ - Otherwise set causes to an array containing only valid CauseSubtype enum values.
155
+ - Do not output free-text causes.
156
+ - Conservative mappings:
157
+ - track/signal/train/power fault -> corresponding *.fault subtype
158
+ - passenger incident -> passenger.incident
159
+ - platform screen door fault -> platform_door.fault
160
+ - engineering/track work -> track.work
161
+ - upgrade/testing/commissioning -> system.upgrade
162
+ - lift/escalator outage -> elevator.outage / escalator.outage
163
+ - "maintenance" by itself is not enough to infer track.work; use null unless a concrete subtype is stated.
164
+ - Deterministic rule: if evidence explicitly mentions testing, integrated systems, commissioning, or preparation for a new stage/opening, set causes to include system.upgrade (not null).
165
+ - If multiple independent causes are explicitly stated, include each unique subtype once.
166
+ `.trim();
167
+ }
168
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"/","sources":["llm/functions/extractClaimsFromNewEvidence/prompt.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoKR,CAAC,IAAI,EAAE,CAAC;AACT,CAAC","sourcesContent":["export function buildSystemPrompt() {\n return `\nYou are an expert assistant that extracts structured impact claims from MRT evidence.\n\nReturn JSON that matches the provided schema exactly.\n\n## Goal\nGiven:\n- Evidence text\n- Evidence timestamp\n\nExtract only operational impact claims (service or station facility impact) that are explicitly stated or strongly implied by the evidence.\n\n## Available tools\n- findLines(lineNames): use to resolve line IDs from line names.\n- findServices(lineId): use to resolve service IDs and service path stations.\n- findStations(stationNames): use to resolve station IDs from station names/codes.\n\nUse tools whenever an ID or station mapping is uncertain. Prefer correctness over guessing.\n\nImportant: Line ID is not service ID. A line can have multiple services. For service claims, always use serviceId from findServices; never use lineId from findLines.\n\nSearch for valid IDs; never invent or fabricate service IDs, line IDs, or station IDs. When searching for lines or services, try several variations (e.g. \"NSL\", \"NS Line\", \"North-South Line\") as special characters and formatting can be sensitive.\n\n## Claim construction rules\n- One claim per affected entity.\n- Use stable IDs returned by tools.\n- If evidence is non-operational noise (links, generic advisory, no new impact fact), return no claims.\n\nIrrelevance gate (strict):\n- Return claims: [] unless the evidence contains at least one concrete operational assertion about impact state (e.g. delay, no-service, reduced-service, service-hours-adjustment, facility outage/degradation, clear/resume, or explicit planned impact window).\n- Do not generate claims from tags/headers alone (e.g. \"[LINE]\", \"UPDATE\", hashtags) without an operational assertion.\n- Advisory-only content (alternative routes, travel advice, support links, \"refer to\" links, reminders) is irrelevant unless it also includes a concrete impact update.\n- If unsure whether new operational state is stated, prefer no claims.\n\n### entity\n- For train line/service disruptions or maintenance, use:\n - { type: \"service\", serviceId }\n - serviceId must come from findServices (not lineId from findLines). Search for a valid service via findServices; do not make up a service ID.\n- For station facility faults (lift/escalator/screen-door), use:\n - { type: \"facility\", stationId, kind }\n\n### statusSignal\n- \"open\": active disruption/degradation now.\n- \"cleared\": evidence says issue resolved/resumed/normal service restored.\n- \"planned\": future scheduled maintenance/planned adjustment.\n\n### effect\nUse object shape with both keys: { service, facility }.\n\nFor service entities:\n- Delayed service -> service: { kind: \"delay\", duration: null unless exact ISO duration is known }\n- Service suspended/closed/no trains -> service: { kind: \"no-service\" }\n- Reduced frequency/capacity -> service: { kind: \"reduced-service\" }\n- Temporary operating-hour changes (start later/end earlier/shortened hours) -> service: { kind: \"service-hours-adjustment\" }\n- If cleared/restored and no ongoing impact remains -> service: null\n- facility must be null\n\nFor facility entities:\n- Facility unavailable -> facility: { kind: \"facility-out-of-service\" }\n- Facility degraded/partially available -> facility: { kind: \"facility-degraded\" }\n- service must be null\n\n### scopes.service\n- For full line/service statements (\"train service resumed\", \"line closed\"), use:\n - [{ type: \"service.whole\" }]\n- For \"between A and B\", use service.segment with station IDs:\n - { type: \"service.segment\", fromStationId, toStationId }\n- For station-specific service impact at one station, use:\n - { type: \"service.point\", stationId }\n- For service entities, scopes.service should generally be non-null.\n- service.segment is directional: fromStationId -> toStationId is an ordered path, not an unordered pair.\n- Validate segment orientation against the specific service path returned by findServices.\n- For bidirectional output, create one claim per directional service and set segment endpoints to match each service direction (reverse endpoints for reverse direction service).\n- Do not copy the same from/to ordering into both directional services unless that ordering is valid for both service paths.\n\nDirection handling:\n- Priority rule: if direction is ambiguous, default to bidirectional. This rule overrides heuristics.\n- Assume service impact is bidirectional unless direction is stated with strong, unambiguous directional wording.\n- Treat directional impact as valid only when phrasing is truly explicit, for example: \"from X to Y towards Z\", \"towards Z only\", \"eastbound only\", \"westbound only\", or equivalent unambiguous one-direction wording.\n- \"from X to Y\" by itself is segment geometry, not directional exclusivity. Do not infer single-direction impact from endpoints alone.\n- Mentions of one segment pair (or one station pair) do not imply one-direction impact by themselves.\n- When direction is not explicitly limited, emit claims for each directional service on the affected line/service.\n- For segment scopes, preserve path direction per service (reverse endpoints for reverse direction service).\n\n### timeHints\nBest effort from evidence. Use null when unknown.\n\ntimeHints shape:\n- timeHints is either null or exactly one of:\n - { kind: \"fixed\", startAt, endAt }\n - { kind: \"recurring\", frequency, startAt, endAt, daysOfWeek, timeWindow, timeZone, excludedDates }\n - { kind: \"start-only\", startAt }\n - { kind: \"end-only\", endAt }\n- Do not use the old nested shape with startAt/endAt/recurring fields.\n\nMaintenance period rule:\n- For maintenance/planned adjustments, timeHints.startAt/timeHints.endAt must represent the overall maintenance window (calendar period), not the daily active service-impact hours.\n- Put repeated daily/weekly impact hours into recurring.timeWindow.\n- For service-hours-adjustment, recurring.timeWindow must represent when impact applies (restricted/no service), NOT normal operating hours.\n- Example: if temporary operating hours are \"first train at 05:45, last train at 23:15\", then service runs during 05:45-23:15 and impact interval is 23:15-05:45; set recurring.timeWindow to { startAt: \"23:15:00\", endAt: \"05:45:00\" }.\n- Never encode the service-running window as recurring.timeWindow for service-hours-adjustment.\n\nWhen choosing kind:\n- Use \"fixed\" when there is a bounded interval (start and maybe end).\n- Use \"recurring\" when there is a repeating pattern.\n- Use \"start-only\" when only a start signal is known.\n- Use \"end-only\" when only an end/clear signal is known.\n\nField guidance:\n- startAt:\n - For newly active disruptions, default to evidence timestamp unless a different start time is stated.\n - For planned items, use the planned start if stated.\n - For \"service will start at X\" or \"first train at X\": the impact window (no service) is from start of day (00:00) until X. Use kind \"fixed\" with startAt at midnight and endAt at the stated start time.\n- endAt:\n - For cleared claims with no start in this evidence, prefer kind \"end-only\" and set endAt to evidence timestamp.\n - For planned windows, use stated/plausible period end when explicit.\n - For kind \"fixed\" and kind \"end-only\", endAt is exclusive.\n- recurring:\n - Populate recurring only for repeating patterns.\n - recurring.startAt and recurring.endAt map to RRULE DTSTART and UNTIL.\n - RRULE UNTIL semantics: recurring.endAt is the last occurrence anchor (inclusive), not the day after.\n - Do not default recurring.startAt/endAt to 00:00:00 unless evidence explicitly indicates all-day or midnight boundaries.\n - Preserve meaningful time-of-day from the described impact window.\n - The anchor-time rule below is specific to maintenance/infra recurring timeHints:\n - recurring.startAt/endAt must anchor to the impact START instant (same clock time as recurring.timeWindow.startAt), not recurring.timeWindow.endAt.\n - For overnight windows, keep start-side anchors across the recurrence range.\n - Example (non-test): if recurring.timeWindow is { startAt: \"22:30:00\", endAt: \"05:00:00\" } on weekdays from 10 Mar to 21 Mar, use startAt/endAt with 22:30 anchors on the first/last applicable dates, not 05:00 anchors.\n - Shape:\n - kind: \"recurring\"\n - frequency: daily | weekly | monthly | yearly\n - startAt, endAt as ISO8601 datetimes with offset\n - daysOfWeek: [MO..SU] or null\n - timeZone: \"Asia/Singapore\"\n - timeWindow: { startAt: \"HH:MM:SS\", endAt: \"HH:MM:SS\" }\n - excludedDates: null unless explicitly stated\n\n## Precision and normalization\n- Prefer explicit facts; avoid inventing causes/details.\n- Use canonical station/service IDs from tools.\n- If only line name is given, map to all relevant services on that line.\n- For ISO8601 datetimes, include timezone offset and seconds. Omit fractional seconds when milliseconds are zero (e.g. use 2026-01-01T07:10:00+08:00, not 2026-01-01T07:10:00.000+08:00).\n- Treat fixed/end-only endAt as exclusive, and recurring.timeWindow.endAt as the end boundary of each daily window.\n- Midnight timestamps are allowed only when explicitly justified by evidence (e.g. \"from 00:00\", \"until midnight\", or clear date-only all-day semantics).\n- Keep claims minimal but complete for downstream state updates.\n- Final self-check before returning:\n - If evidence has no unambiguous one-direction qualifier, ensure claims include all directional services for the affected service/line.\n - Only return a single directional service claim when explicit direction-only wording is present.\n- Do not include commentary, only schema-conforming JSON.\n\n### causes\n- causes must always be present on every claim.\n- Use null when the evidence does not state or strongly imply a concrete cause subtype.\n- Otherwise set causes to an array containing only valid CauseSubtype enum values.\n- Do not output free-text causes.\n- Conservative mappings:\n - track/signal/train/power fault -> corresponding *.fault subtype\n - passenger incident -> passenger.incident\n - platform screen door fault -> platform_door.fault\n - engineering/track work -> track.work\n - upgrade/testing/commissioning -> system.upgrade\n - lift/escalator outage -> elevator.outage / escalator.outage\n- \"maintenance\" by itself is not enough to infer track.work; use null unless a concrete subtype is stated.\n- Deterministic rule: if evidence explicitly mentions testing, integrated systems, commissioning, or preparation for a new stage/opening, set causes to include system.upgrade (not null).\n- If multiple independent causes are explicitly stated, include each unique subtype once.\n`.trim();\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import z from 'zod';
2
+ import type { MRTDownRepository } from '#repo/MRTDownRepository.js';
3
+ import { Tool } from '../../../common/tool.js';
4
+ declare const FindLinesToolParametersSchema: z.ZodObject<{
5
+ lineNames: z.ZodArray<z.ZodString>;
6
+ }, z.z.core.$strip>;
7
+ type FindLinesToolParameters = z.infer<typeof FindLinesToolParametersSchema>;
8
+ export declare class FindLinesTool extends Tool<FindLinesToolParameters> {
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): FindLinesToolParameters;
17
+ runner(params: FindLinesToolParameters): Promise<string>;
18
+ }
19
+ export {};
@@ -0,0 +1,65 @@
1
+ import { gfmToMarkdown } from 'mdast-util-gfm';
2
+ import { toMarkdown } from 'mdast-util-to-markdown';
3
+ import z from 'zod';
4
+ import { Tool } from '../../../common/tool.js';
5
+ const FindLinesToolParametersSchema = z.object({
6
+ lineNames: z.array(z.string()),
7
+ });
8
+ export class FindLinesTool extends Tool {
9
+ name = 'findLines';
10
+ description = 'Find lines by name';
11
+ repo;
12
+ constructor(repo) {
13
+ super();
14
+ this.repo = repo;
15
+ }
16
+ get paramsSchema() {
17
+ return z.toJSONSchema(FindLinesToolParametersSchema);
18
+ }
19
+ parseParams(params) {
20
+ return FindLinesToolParametersSchema.parse(params);
21
+ }
22
+ async runner(params) {
23
+ console.log('[findLines] Calling tool with parameters:', params);
24
+ const lines = this.repo.lines.searchByName(params.lineNames);
25
+ const table = {
26
+ type: 'table',
27
+ children: [
28
+ {
29
+ type: 'tableRow',
30
+ children: [
31
+ {
32
+ type: 'tableCell',
33
+ children: [{ type: 'text', value: 'Line ID' }],
34
+ },
35
+ {
36
+ type: 'tableCell',
37
+ children: [{ type: 'text', value: 'Line Name' }],
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ };
43
+ for (const line of lines) {
44
+ table.children.push({
45
+ type: 'tableRow',
46
+ children: [
47
+ {
48
+ type: 'tableCell',
49
+ children: [{ type: 'text', value: line.id }],
50
+ },
51
+ {
52
+ type: 'tableCell',
53
+ children: [{ type: 'text', value: line.name['en-SG'] }],
54
+ },
55
+ ],
56
+ });
57
+ }
58
+ const output = toMarkdown(table, {
59
+ extensions: [gfmToMarkdown()],
60
+ });
61
+ console.log(`[findLines] Response output:\n${output}`);
62
+ return output;
63
+ }
64
+ }
65
+ //# sourceMappingURL=FindLinesTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FindLinesTool.js","sourceRoot":"/","sources":["llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CAC/B,CAAC,CAAC;AAGH,MAAM,OAAO,aAAc,SAAQ,IAA6B;IACvD,IAAI,GAAG,WAAW,CAAC;IACnB,WAAW,GAAG,oBAAoB,CAAC;IACzB,IAAI,CAAoB;IAEzC,YAAY,IAAuB;QACjC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,IAAW,YAAY;QACrB,OAAO,CAAC,CAAC,YAAY,CAAC,6BAA6B,CAAC,CAAC;IACvD,CAAC;IAEM,WAAW,CAAC,MAAe;QAChC,OAAO,6BAA6B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,MAA+B;QACjD,OAAO,CAAC,GAAG,CAAC,2CAA2C,EAAE,MAAM,CAAC,CAAC;QAEjE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE7D,MAAM,KAAK,GAAU;YACnB,IAAI,EAAE,OAAO;YACb,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,UAAU;oBAChB,QAAQ,EAAE;wBACR;4BACE,IAAI,EAAE,WAAW;4BACjB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;yBAC/C;wBACD;4BACE,IAAI,EAAE,WAAW;4BACjB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;yBACjD;qBACF;iBACF;aACF;SACF,CAAC;QAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAClB,IAAI,EAAE,UAAU;gBAChB,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,WAAW;wBACjB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;qBAC7C;oBACD;wBACE,IAAI,EAAE,WAAW;wBACjB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;qBACxD;iBACF;aACF,CAAC,CAAC;QACL,CAAC;QAED,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE;YAC/B,UAAU,EAAE,CAAC,aAAa,EAAE,CAAC;SAC9B,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,iCAAiC,MAAM,EAAE,CAAC,CAAC;QAEvD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import type { Table } from 'mdast';\nimport { gfmToMarkdown } from 'mdast-util-gfm';\nimport { toMarkdown } from 'mdast-util-to-markdown';\nimport z from 'zod';\nimport type { MRTDownRepository } from '#repo/MRTDownRepository.js';\nimport { Tool } from '../../../common/tool.js';\n\nconst FindLinesToolParametersSchema = z.object({\n lineNames: z.array(z.string()),\n});\ntype FindLinesToolParameters = z.infer<typeof FindLinesToolParametersSchema>;\n\nexport class FindLinesTool extends Tool<FindLinesToolParameters> {\n public name = 'findLines';\n public description = 'Find lines by name';\n private readonly repo: MRTDownRepository;\n\n constructor(repo: MRTDownRepository) {\n super();\n this.repo = repo;\n }\n\n public get paramsSchema(): { [key: string]: unknown } {\n return z.toJSONSchema(FindLinesToolParametersSchema);\n }\n\n public parseParams(params: unknown): FindLinesToolParameters {\n return FindLinesToolParametersSchema.parse(params);\n }\n\n public async runner(params: FindLinesToolParameters): Promise<string> {\n console.log('[findLines] Calling tool with parameters:', params);\n\n const lines = this.repo.lines.searchByName(params.lineNames);\n\n const table: Table = {\n type: 'table',\n children: [\n {\n type: 'tableRow',\n children: [\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Line ID' }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Line Name' }],\n },\n ],\n },\n ],\n };\n\n for (const line of lines) {\n table.children.push({\n type: 'tableRow',\n children: [\n {\n type: 'tableCell',\n children: [{ type: 'text', value: line.id }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: line.name['en-SG'] }],\n },\n ],\n });\n }\n\n const output = toMarkdown(table, {\n extensions: [gfmToMarkdown()],\n });\n console.log(`[findLines] Response output:\\n${output}`);\n\n return output;\n }\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { DateTime } from 'luxon';
2
+ import z from 'zod';
3
+ import type { MRTDownRepository } from '#repo/MRTDownRepository.js';
4
+ import { Tool } from '../../../common/tool.js';
5
+ declare const FindServicesToolParametersSchema: z.ZodObject<{
6
+ lineId: z.ZodString;
7
+ }, z.z.core.$strip>;
8
+ type FindServicesToolParameters = z.infer<typeof FindServicesToolParametersSchema>;
9
+ export declare class FindServicesTool extends Tool<FindServicesToolParameters> {
10
+ name: string;
11
+ description: string;
12
+ private readonly evidenceTs;
13
+ private readonly repo;
14
+ constructor(evidenceTs: DateTime, repo: MRTDownRepository);
15
+ get paramsSchema(): {
16
+ [key: string]: unknown;
17
+ };
18
+ parseParams(params: unknown): FindServicesToolParameters;
19
+ runner(params: FindServicesToolParameters): Promise<string>;
20
+ }
21
+ export {};
@@ -0,0 +1,115 @@
1
+ import { DateTime } from 'luxon';
2
+ import { gfmToMarkdown } from 'mdast-util-gfm';
3
+ import { toMarkdown } from 'mdast-util-to-markdown';
4
+ import z from 'zod';
5
+ import { assert } from '#util/assert.js';
6
+ import { Tool } from '../../../common/tool.js';
7
+ const FindServicesToolParametersSchema = z.object({
8
+ lineId: z.string(),
9
+ });
10
+ export class FindServicesTool extends Tool {
11
+ name = 'findServices';
12
+ description = 'Find services by name';
13
+ evidenceTs;
14
+ repo;
15
+ constructor(evidenceTs, repo) {
16
+ super();
17
+ this.evidenceTs = evidenceTs;
18
+ this.repo = repo;
19
+ }
20
+ get paramsSchema() {
21
+ return z.toJSONSchema(FindServicesToolParametersSchema);
22
+ }
23
+ parseParams(params) {
24
+ return FindServicesToolParametersSchema.parse(params);
25
+ }
26
+ async runner(params) {
27
+ console.log('[findServices] Calling tool with parameters:', params);
28
+ const services = this.repo.services.searchByLineId(params.lineId);
29
+ const table = {
30
+ type: 'table',
31
+ children: [
32
+ {
33
+ type: 'tableRow',
34
+ children: [
35
+ {
36
+ type: 'tableCell',
37
+ children: [{ type: 'text', value: 'Service ID' }],
38
+ },
39
+ {
40
+ type: 'tableCell',
41
+ children: [{ type: 'text', value: 'Service Name' }],
42
+ },
43
+ {
44
+ type: 'tableCell',
45
+ children: [{ type: 'text', value: 'Line ID' }],
46
+ },
47
+ {
48
+ type: 'tableCell',
49
+ children: [{ type: 'text', value: 'Station IDs' }],
50
+ },
51
+ {
52
+ type: 'tableCell',
53
+ children: [{ type: 'text', value: 'Operating Hours' }],
54
+ },
55
+ ],
56
+ },
57
+ ],
58
+ };
59
+ for (const service of services) {
60
+ const relevantRevision = service.revisions.findLast((revision) => {
61
+ const startAt = DateTime.fromISO(revision.startAt);
62
+ assert(startAt.isValid, `Invalid date: ${revision.startAt}`);
63
+ if (revision.endAt == null) {
64
+ return startAt <= this.evidenceTs;
65
+ }
66
+ const endAt = DateTime.fromISO(revision.endAt);
67
+ assert(endAt.isValid, `Invalid date: ${revision.endAt}`);
68
+ return startAt <= this.evidenceTs && endAt > this.evidenceTs;
69
+ });
70
+ if (relevantRevision == null)
71
+ continue;
72
+ table.children.push({
73
+ type: 'tableRow',
74
+ children: [
75
+ {
76
+ type: 'tableCell',
77
+ children: [{ type: 'text', value: service.id }],
78
+ },
79
+ {
80
+ type: 'tableCell',
81
+ children: [{ type: 'text', value: service.name['en-SG'] }],
82
+ },
83
+ {
84
+ type: 'tableCell',
85
+ children: [{ type: 'text', value: service.lineId }],
86
+ },
87
+ {
88
+ type: 'tableCell',
89
+ children: [
90
+ {
91
+ type: 'text',
92
+ value: `${relevantRevision.path.stations.length} (${relevantRevision.path.stations.map((station) => station.stationId).join('→')})`,
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ type: 'tableCell',
98
+ children: [
99
+ {
100
+ type: 'text',
101
+ value: `Weekdays: ${relevantRevision.operatingHours.weekdays.start}-${relevantRevision.operatingHours.weekdays.end} | Weekends: ${relevantRevision.operatingHours.weekends.start}-${relevantRevision.operatingHours.weekends.end}`,
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ });
107
+ }
108
+ const output = toMarkdown(table, {
109
+ extensions: [gfmToMarkdown()],
110
+ });
111
+ console.log(`[findServices] Response output:\n${output}`);
112
+ return output;
113
+ }
114
+ }
115
+ //# sourceMappingURL=FindServicesTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FindServicesTool.js","sourceRoot":"/","sources":["llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport type { Table } from 'mdast';\nimport { gfmToMarkdown } from 'mdast-util-gfm';\nimport { toMarkdown } from 'mdast-util-to-markdown';\nimport z from 'zod';\nimport type { MRTDownRepository } from '#repo/MRTDownRepository.js';\nimport { assert } from '#util/assert.js';\nimport { Tool } from '../../../common/tool.js';\n\nconst FindServicesToolParametersSchema = z.object({\n lineId: z.string(),\n});\ntype FindServicesToolParameters = z.infer<\n typeof FindServicesToolParametersSchema\n>;\n\nexport class FindServicesTool extends Tool<FindServicesToolParameters> {\n public name = 'findServices';\n public description = 'Find services by name';\n private readonly evidenceTs: DateTime;\n private readonly repo: MRTDownRepository;\n\n constructor(evidenceTs: DateTime, repo: MRTDownRepository) {\n super();\n this.evidenceTs = evidenceTs;\n this.repo = repo;\n }\n\n public get paramsSchema(): { [key: string]: unknown } {\n return z.toJSONSchema(FindServicesToolParametersSchema);\n }\n\n public parseParams(params: unknown): FindServicesToolParameters {\n return FindServicesToolParametersSchema.parse(params);\n }\n\n public async runner(params: FindServicesToolParameters): Promise<string> {\n console.log('[findServices] Calling tool with parameters:', params);\n\n const services = this.repo.services.searchByLineId(params.lineId);\n\n const table: Table = {\n type: 'table',\n children: [\n {\n type: 'tableRow',\n children: [\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Service ID' }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Service Name' }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Line ID' }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Station IDs' }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: 'Operating Hours' }],\n },\n ],\n },\n ],\n };\n\n for (const service of services) {\n const relevantRevision = service.revisions.findLast((revision) => {\n const startAt = DateTime.fromISO(revision.startAt);\n assert(startAt.isValid, `Invalid date: ${revision.startAt}`);\n\n if (revision.endAt == null) {\n return startAt <= this.evidenceTs;\n }\n\n const endAt = DateTime.fromISO(revision.endAt);\n assert(endAt.isValid, `Invalid date: ${revision.endAt}`);\n\n return startAt <= this.evidenceTs && endAt > this.evidenceTs;\n });\n\n if (relevantRevision == null) continue;\n\n table.children.push({\n type: 'tableRow',\n children: [\n {\n type: 'tableCell',\n children: [{ type: 'text', value: service.id }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: service.name['en-SG'] }],\n },\n {\n type: 'tableCell',\n children: [{ type: 'text', value: service.lineId }],\n },\n {\n type: 'tableCell',\n children: [\n {\n type: 'text',\n value: `${relevantRevision.path.stations.length} (${relevantRevision.path.stations.map((station) => station.stationId).join('→')})`,\n },\n ],\n },\n {\n type: 'tableCell',\n children: [\n {\n type: 'text',\n value: `Weekdays: ${relevantRevision.operatingHours.weekdays.start}-${relevantRevision.operatingHours.weekdays.end} | Weekends: ${relevantRevision.operatingHours.weekends.start}-${relevantRevision.operatingHours.weekends.end}`,\n },\n ],\n },\n ],\n });\n }\n\n const output = toMarkdown(table, {\n extensions: [gfmToMarkdown()],\n });\n console.log(`[findServices] Response output:\\n${output}`);\n\n return output;\n }\n}\n"]}