casefile-repository-mcp-server
Version:
Vessel casefile management system with MCP integration for maritime operations
2,272 lines • 111 kB
JavaScript
import { getMongoClient } from "../utils/mongodb.js";
import { getTypesenseClient } from "../utils/typesense.js";
import { config } from "../utils/config.js";
import axios from "axios";
import { ObjectId } from "mongodb";
import { logger } from "../utils/logger.js";
// Helper function to get full email details from MongoDB
async function get_full_email_details(emailId) {
try {
if (!emailId) {
throw new Error("Email ID is required");
}
let objectId;
try {
objectId = new ObjectId(emailId);
}
catch (error) {
throw new Error(`Invalid email ID format: ${emailId}`);
}
logger.info(`[get_full_email_details] Searching for email with ID: ${emailId}`);
logger.info(`[get_full_email_details] Using MongoDB URI: ${config.etlDevMongoUri}`);
logger.info(`[get_full_email_details] Using DB name: ${config.etlDevDbName}`);
const mongoClient = await getMongoClient(config.etlDevMongoUri);
const db = mongoClient.db(config.etlDevDbName);
// Try to find the email in mail_temp collection first
let email = await db.collection("mail_temp").findOne({ _id: objectId });
// If not found, try mail_archive collection
if (!email) {
email = await db.collection("mail_archive").findOne({ _id: objectId });
}
if (!email) {
return {
found: false,
message: `No email found with ID: ${emailId}`
};
}
return {
found: true,
source: email._collection || "unknown",
email: email
};
}
catch (error) {
console.error(`[get_full_email_details] Error: ${error}`);
return {
found: false,
error: error instanceof Error ? error.message : String(error),
message: `Failed to retrieve email details: ${error instanceof Error ? error.message : String(error)}`
};
}
}
// Internal utility function for vessel details lookup (not exposed as tool)
async function getVesselDetails(query) {
try {
if (!query) {
throw new Error("Query parameter is required for vessel details search");
}
console.log(`[Internal] Searching for vessel details with vessel name: ${query}`);
// Set up search parameters for the fleet-vessel-lookup collection
const search_parameters = {
q: query,
query_by: 'vesselName',
collection: 'fleet-vessel-lookup',
per_page: 1,
include_fields: 'vesselName,imo,class,flag,shippalmDoc,isV3',
prefix: false,
num_typos: 2,
};
// Execute search
const typesenseClient = getTypesenseClient(config);
const raw = await typesenseClient.collections('fleet-vessel-lookup').documents().search(search_parameters);
const hits = raw.hits || [];
if (!hits || hits.length === 0) {
return null;
}
// Process and format results
const doc = hits[0].document || {};
return {
vesselName: doc.vesselName,
imo: doc.imo,
class: doc.class,
flag: doc.flag,
shippalmDoc: doc.shippalmDoc,
isV3: doc.isV3,
score: hits[0].text_match || 0
};
}
catch (error) {
console.error(`[Internal] Error searching for vessel details: ${error}`);
throw error;
}
}
// --- Write Casefile Data Tool Implementation ---
// Helper to push casefile to Typesense
async function pushToTypesense(res, action) {
const id = res.id || res._id?.toString();
const casefileTxt = res.casefile;
const summaryTxt = res.summary;
const embeddingText = `Below casefile ${casefileTxt} with following summary ${summaryTxt} `;
const link = await generateCasefileWeblink(id);
// Update the casefile in MongoDB with the link
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
await collection.updateOne({ _id: id }, { $set: { link } });
// Prepare Typesense data
const createdAt = res.createdAt instanceof Date ? Math.floor(res.createdAt.getTime() / 1000) : (typeof res.createdAt === 'number' ? res.createdAt : Date.now() / 1000);
const updatedAt = res.updatedAt instanceof Date ? Math.floor(res.updatedAt.getTime() / 1000) : (typeof res.updatedAt === 'number' ? res.updatedAt : Date.now() / 1000);
const importance = typeof res.importance === 'number' ? res.importance : 0;
const data = {
id: id,
_id: id,
casefile: res.casefile,
currentStatus: res.currentStatus,
casefileInitiationDate: createdAt,
category: res.category,
conversationTopic: [],
embedding_text: embeddingText,
imo: Number(res.imo),
importance: String(importance), // <-- convert to string for Typesense
importance_score: importance, // <-- keep as number for other uses
lastcasefileUpdateDate: updatedAt,
summary: res.summary,
vesselId: res.vesselId ? String(res.vesselId) : null,
vesselName: res.vesselName ? String(res.vesselName) : null,
link: link,
followUp: res.followUp || "",
pages: JSON.stringify((res.pages || []).slice(-2)),
index: JSON.stringify((res.index || []).slice(-2)),
};
if (res.plan_status) {
data.plan_status = res.plan_status;
}
try {
const typesenseClient = getTypesenseClient(config);
console.info(`Data pushed to Typesense:`, data);
const result = await typesenseClient.collections("emailCasefile").documents().import([data], { action });
// Log after Typesense push
console.info(`[pushToTypesense] Data pushed to Typesense successfully for id: ${id}`);
console.info(result);
return result;
}
catch (e) {
if (typeof e === "object" && e !== null && "importResults" in e) {
console.error("Typesense import error details:", e.importResults);
}
throw e;
}
}
// Internal: create_casefile implementation
async function create_casefile(args) {
const casefileName = args.casefileName || null;
const casefileSummary = args.casefileSummary || null;
const currentStatus = args.currentStatus || null;
const originalImportance = args.importance ?? 0;
const category = args.category || "General";
const role = args.role || null;
const imo = args.imo || null;
let vesselName = null;
let vesselId = null;
if (imo) {
try {
const vesselDetails = await getVesselDetails(imo.toString());
vesselName = vesselDetails?.vesselName || null;
vesselId = vesselDetails?.id || null;
}
catch (e) {
vesselName = null;
vesselId = null;
}
}
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
// Just search if that casefile is already there
let result = null;
if (category !== "General") {
result = await collection.findOne({ imo: imo, category: category });
}
let casefileId = result ? result._id : null;
let alreadyExists = !!result;
// Prepare data
let data = {
vesselId: vesselId,
imo: imo,
vesselName: vesselName,
casefile: casefileName,
currentStatus: currentStatus,
summary: casefileSummary,
originalImportance: originalImportance,
importance: originalImportance,
category: category,
role: role,
followUp: "",
createdAt: new Date(),
updatedAt: new Date(),
index: [],
pages: []
};
if (casefileId) {
delete data.createdAt;
delete data.originalImportance;
delete data.index;
delete data.pages;
delete data.followUp;
}
// Insert or update
let mongoResult;
if (casefileId) {
mongoResult = await collection.updateOne({ _id: casefileId }, { $set: data });
}
else {
mongoResult = await collection.insertOne(data);
casefileId = mongoResult.insertedId;
}
// Log after MongoDB push
console.info(`[create_casefile] Data pushed to MongoDB:`, { casefileId, data });
// Generate casefile URL and update
const casefileUrl = await generateCasefileWeblink(casefileId.toString());
await collection.updateOne({ _id: casefileId }, { $set: { link: casefileUrl } });
// If not already exists, push to Typesense
if (!alreadyExists) {
try {
await pushToTypesense({ ...data, _id: casefileId }, 'create');
}
catch (e) {
console.error(`Error importing data to Typesense: ${e}`);
}
}
return {
type: "text",
text: `Casefile created with casefile url: ${casefileUrl}`
};
}
// Internal: update_casefile implementation
async function update_casefile(args) {
const { casefile_url, casefileSummary, importance, tags: tagsArg, topic, summary: summaryArg, mailId, currentStatus, casefileName, facts, links: linksArg, detailed_report } = args;
let plan_status = "unprocessed";
let tags = tagsArg || [];
let summary = summaryArg || "";
let links = Array.isArray(linksArg) ? linksArg.map((i) => ({ link: i })) : [];
// Helper: markdown_to_html_link stub
function markdown_to_html_link(md) {
// Replace with actual markdown-to-HTML-link logic if needed
return md ? `<a href='#'>${md.substring(0, 32)}...</a>` : "";
}
// Add detailed_report as first link
links = [{ link: markdown_to_html_link(detailed_report || "") }, ...links];
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
if (!casefile_url) {
throw new Error("Casefile URL is required");
}
let casefile_id = casefile_url;
// If not a valid ObjectId, try to resolve from link (stub for link_to_id)
if (!ObjectId.isValid(casefile_url)) {
// Implement link_to_id if needed; for now, throw
throw new Error("Valid Casefile ID is required");
}
// Normalize tags
if (typeof tags === "string") {
tags = [tags];
}
// If facts present, append to summary
if (facts) {
summary = summary + " <br> " + facts;
}
// Build aggregation pipeline
const now = new Date();
const update_pipeline = [];
// Stage 1: Conditional base field updates
const set_stage = { updatedAt: now };
if (casefileName !== undefined)
set_stage.casefile = casefileName;
if (currentStatus !== undefined)
set_stage.currentStatus = currentStatus;
if (casefileSummary !== undefined)
set_stage.summary = casefileSummary;
if (importance !== undefined)
set_stage.importance = importance;
if (plan_status !== undefined)
set_stage.plan_status = plan_status;
if (Object.keys(set_stage).length > 0) {
update_pipeline.push({ $set: set_stage });
}
// Stage 2: Ensure arrays exist and compute new pagenum
update_pipeline.push({
$set: {
pages: { $ifNull: ["$pages", []] },
index: { $ifNull: ["$index", []] },
_nextPageNum: {
$add: [
{
$max: [
{ $ifNull: [{ $max: "$pages.pagenum" }, 0] },
{ $ifNull: [{ $max: "$index.pagenum" }, 0] }
]
},
1
]
}
}
});
// Stage 3: Update tags as a unique set
if (tags && tags.length > 0) {
update_pipeline.push({
$set: {
tags: { $setUnion: [{ $ifNull: ["$tags", []] }, tags] }
}
});
}
// Stage 4: Append to pages and index arrays
update_pipeline.push({
$set: {
pages: {
$concatArrays: [
"$pages",
[
{
pagenum: "$_nextPageNum",
summary: summary,
createdAt: now,
subject: topic,
flag: topic,
type: "SIYA",
link: links,
plan_status: plan_status
}
]
]
},
index: {
$concatArrays: [
"$index",
[
{
pagenum: "$_nextPageNum",
type: "SIYA",
createdAt: now,
topic: topic,
plan_status: plan_status
}
]
]
}
}
});
// Stage 5: Cleanup temporary field
update_pipeline.push({ $unset: "_nextPageNum" });
// Execute update
await collection.updateOne({ _id: new ObjectId(casefile_id) }, update_pipeline // MongoDB Node driver supports pipeline updates with 'as any'
);
// Fetch updated document
const mongoresult = await collection.findOne({ _id: new ObjectId(casefile_id) });
if (!mongoresult) {
throw new Error(`No casefile found with ID: ${casefile_id} after update.`);
}
// Push to Typesense
try {
await pushToTypesense(mongoresult, 'upsert');
}
catch (e) {
console.error(`Error updating casefile in Typesense: ${e}`);
}
// Return text response
return {
type: "text",
text: `Casefile updated with casefile url: ${mongoresult.link}`
};
}
// Dispatcher for write operations
async function write_casefile_data(args) {
const op = args.operation;
if (op === "write_casefile") {
return await create_casefile(args);
}
if (op === "write_page") {
return await update_casefile(args);
}
throw new Error(`Unsupported operation for write_casefile_data: '${op}'`);
}
// Tool Definitions
export const toolDefinitions = [
{
name: "get_casefile_index",
description: "Get the casefile index",
inputSchema: {
type: "object",
properties: {
casefile_id: {
type: "string",
description: "ID of the casefile"
},
limit: {
type: "number",
description: "Max log entries to return.",
minimum: 1,
default: 10
},
next_iter: {
type: "number",
description: "The pagination parameter. If 0, returns the latest entries. If 1, returns the next batch of older entries, and so on.",
default: 0
}
},
required: ["casefile_id"]
},
handler: async (args) => {
try {
const casefile_id = args.casefile_id;
if (!casefile_id) {
throw new Error("casefile_id is required");
}
const limit = args.limit || 10;
const next_iter = args.next_iter || 0;
console.log(`Getting casefile index for: ${casefile_id}, limit: ${limit}, next_iter: ${next_iter}`);
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
// Get total entries count
const pipelineLength = [
{ $match: { _id: new ObjectId(casefile_id) } },
{ $project: { index: { $size: "$index" } } }
];
const result = await collection.aggregate(pipelineLength).toArray();
if (!result || result.length === 0) {
return [{
type: "text",
text: `No casefile found with ID: ${casefile_id}`
}];
}
const total_entries = result[0].index;
let skip;
if (next_iter === 0) {
skip = Math.max(0, total_entries - limit);
}
else {
skip = Math.max(0, total_entries - (limit * (next_iter + 1)));
if (skip < 0)
skip = 0;
}
const entries_to_take = Math.min(limit, total_entries - skip);
if (entries_to_take <= 0) {
return [{
type: "text",
text: `No more index entries to retrieve for casefile ID: ${casefile_id}`
}];
}
const pipelineFetch = [
{ $match: { _id: new ObjectId(casefile_id) } },
{
$project: {
index: { $slice: ["$index", skip, entries_to_take] },
category: 1,
imo: 1,
link: 1,
casefile: 1, // ✅ Add casefile title
_id: 1
}
}
];
const fetchResult = await collection.aggregate(pipelineFetch).toArray();
if (!fetchResult || fetchResult.length === 0) {
return [{
type: "text",
text: `Failed to retrieve index entries for casefile ID: ${casefile_id}`
}];
}
const document = fetchResult[0];
const response = {
casefile_id: document._id.toString(),
index: document.index || [],
category: document.category,
imo: document.imo,
link: document.link,
total_entries: total_entries,
current_batch: next_iter,
total_batches: Math.max(1, Math.ceil(total_entries / limit)),
has_more: skip > 0
};
// ✅ Artifact from casefile-level link
const link_data = document.link
? [{ title: document.casefile || `Casefile ${casefile_id}`, url: document.link }]
: [];
const artifacts = getListOfArtifacts(document.casefile || `casefile index ${casefile_id}`, link_data);
return [
{
type: "text",
text: JSON.stringify(response, null, 2),
title: `Casefile Index for ${casefile_id}`,
format: "json"
},
...artifacts
];
}
catch (error) {
throw new Error(`Error getting casefile index: ${error}`);
}
}
},
{
name: "get_casefile_pages",
description: "Get specific pages from a casefile by page number (1-based). For example, [1, 2, 3] returns the first three pages; [2, 5] returns only pages 2 and 5 if they exist.",
inputSchema: {
type: "object",
properties: {
casefile_id: {
type: "string",
description: "ID of the casefile"
},
pages: {
type: "array",
description: "List of 1-based page numbers to fetch. Can be contiguous (e.g. [1, 2, 3]) or sparse (e.g. [2, 5]).",
items: {
type: "number",
minimum: 1
},
minItems: 1
}
},
required: ["casefile_id", "pages"]
},
handler: async (args) => {
try {
const casefile_id = args.casefile_id;
let page_list = args.pages || [];
if (!Array.isArray(page_list) || page_list.length === 0) {
throw new Error("pages must be a non-empty list");
}
try {
page_list = [...new Set(page_list.map(p => parseInt(p.toString())))].sort((a, b) => a - b);
}
catch (error) {
throw new Error("All entries in 'pages' must be integers or castable to integers");
}
if (!casefile_id) {
throw new Error("casefile_id is required");
}
console.log(`Getting casefile pages for: ${casefile_id}, pages: ${page_list}`);
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
const doc = await collection.findOne({ _id: new ObjectId(casefile_id) }, { projection: { link: 1, casefile: 1 } });
if (!doc) {
return [{
type: "text",
text: `No casefile found with ID: ${casefile_id}`
}];
}
const page_indices = page_list.map(p => p - 1); // convert to 0-based
const min_index = Math.min(...page_indices);
const max_index = Math.max(...page_indices);
const slice_start = Math.max(0, min_index);
const slice_count = max_index - min_index + 1;
console.log(`Mongo slice: start=${slice_start}, count=${slice_count}`);
const pipeline = [
{ $match: { _id: new ObjectId(casefile_id) } },
{
$project: {
pages: { $slice: ["$pages", slice_start, slice_count] },
_id: 0
}
}
];
const result = await collection.aggregate(pipeline).toArray();
if (!result || result.length === 0) {
return [{
type: "text",
text: "No pages found for the given casefile ID."
}];
}
const sliced_pages = result[0].pages || [];
const requested_pages = [];
for (const page_num of page_list) {
const actual_index = page_num - 1; // 0-based
const i = actual_index - slice_start;
if (i >= 0 && i < sliced_pages.length) {
requested_pages.push(sliced_pages[i]);
}
}
if (requested_pages.length === 0) {
return [{
type: "text",
text: "No pages matched the requested indices"
}];
}
const response = {
casefile_id: casefile_id,
pages_requested: page_list,
pages_returned: requested_pages,
link: doc.link
};
const link_data = doc.link
? [{ title: doc.casefile || `Casefile ${casefile_id}`, url: doc.link }]
: [];
const artifacts = getListOfArtifacts(doc.casefile || `casefile pages ${casefile_id}`, link_data);
return [
{
type: "text",
text: JSON.stringify(response, null, 2),
title: `Casefile Pages for ${casefile_id}`,
format: "json"
},
...artifacts
];
}
catch (error) {
throw new Error(`Error getting casefile pages: ${error}`);
}
}
},
{
name: "get_latest_plan",
description: "Get the latest plan for the casefile, the tool only accepts the casefile id whcih is the mongo db id of the case fiel , do not provide any other id like imo number or vessel name, that will not work.",
inputSchema: {
type: "object",
properties: {
casefile_id: {
type: "string",
description: "ID of the casefile it will the mongo db id of the case file."
}
},
required: ["casefile_id"]
},
handler: async (args) => {
try {
const casefile_id = args.casefile_id;
if (!casefile_id) {
throw new Error("casefile_id is required");
}
console.log(`Getting latest plan for casefile: ${casefile_id}`);
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
const document = await collection.findOne({ _id: new ObjectId(casefile_id) }, { projection: { casefilePlans: 1 } });
if (!document) {
return [{
type: "text",
text: `No casefile found with ID: ${casefile_id}`
}];
}
const plans = document.casefilePlans || [];
if (!plans || !Array.isArray(plans) || plans.length === 0) {
return [{
type: "text",
text: "No plans found for the casefile"
}];
}
const sorted_plans = plans.sort((a, b) => {
const dateA = a.dateTime || "";
const dateB = b.dateTime || "";
return dateB.localeCompare(dateA);
});
const latest_plan = sorted_plans[0];
const result = {
casefile_id: casefile_id,
latest_plan: {
dateTime: latest_plan.dateTime,
flag: latest_plan.flag,
plan: latest_plan.plan
}
};
return [{
type: "text",
text: JSON.stringify(result, null, 2),
title: `Latest Plan for ${casefile_id}`,
format: "json"
}];
}
catch (error) {
throw new Error(`Error getting latest plan: ${error}`);
}
}
},
{
name: "smart_casefile_search",
description: `Universal, adaptive search tool for vessel case-files. Supports free-text semantic search, hybrid search, and fine-grained filtering by vessel, category, importance, plan status, and date-range.
**PAGINATION & RESULTS:**
- Returns up to 'max_results' (default: 10, max: 20) case-files per request
- Response ALWAYS includes:
• 'found': Total number of matching case-files in the entire database
• 'has_more': Boolean indicating if more results exist beyond current page
• 'hits': Array containing the actual case-file results for current page
• 'page': Current page number
• 'per_page': Number of results in current page
**TO GET MORE RESULTS:**
- If 'has_more' is true, increment 'page' parameter (e.g., page: 2, page: 3, etc.)
- Example: If search returns found: 47, page: 1, per_page: 10, has_more: true
→ This means you're seeing results 1-10 of 47 total
→ Set page: 2 to get results 11-20
→ Set page: 3 to get results 21-30, and so on
**VESSEL NAME HANDLING:**
✔️ If the user mentions a vessel **name**, ALWAYS:
1. Call 'get_vessel_details' to convert that name to its IMO number
2. Pass the returned IMO to the 'imo' parameter so that only case-files for the correct vessel are returned`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural-language query. Leave blank to browse all case-files for the given filters. Example: 'main engine vibration root cause'"
},
search_type: {
type: "string",
description: "Search strategy: • 'semantic' → vector search on the 'embedding' field for conceptual matching • 'hybrid' → combines semantic search with keyword matching across embedding, text content, and vessel name fields for comprehensive results. Choose automatically based on the user's intent; default is 'hybrid'.",
enum: ["semantic", "hybrid"],
default: "hybrid"
},
// Flattened filter properties
imo: {
type: "number",
description: "IMO number for vessel-specific searches. **Required** whenever the user's query refers to a specific vessel name (convert via 'get_vessel_details')."
},
vessel_name: {
type: "string",
description: "Vessel name (case-insensitive). Used only when IMO is not yet known; tool will internally resolve name→IMO."
},
category: {
type: "string",
enum: [
"General",
"LocationAndCargoActivity",
"sire",
"loReport",
"internalAudit",
"vir",
"FleetAlert",
"SmartShipAlert",
"fleetAlert",
"smartShipAlert",
"locationAndCargoActivity",
"classSurveyAndCertificateStatus"
],
description: "Category tag (must match one of the enum values)."
},
importance: {
type: "string",
enum: ["Medium", "Low", "High"],
description: "Importance of the case file (must be one of: Medium, Low, High)"
},
from_date: {
type: "string",
description: "Include case-files with 'lastCasefileUpdateDate' **on or after** this date (dd/mm/yyyy)."
},
to_date: {
type: "string",
description: "Include case-files with 'lastCasefileUpdateDate' **up to** this date (dd/mm/yyyy)."
},
sort_by: {
type: "string",
description: "Sort order: • 'relevance' → by search score/relevance (default) • 'recent' → 'lastCasefileUpdateDate' desc • 'oldest' → 'lastCasefileUpdateDate' asc",
enum: [
"relevance",
"recent",
"oldest"
],
default: "relevance"
},
max_results: {
type: "number",
description: `Number of results to return PER PAGE (not total).
• Default: 10 results per page
• Range: 1-20 results per page
• To get MORE than 20 results, use multiple requests with increasing 'page' numbers
• Example: max_results: 10, page: 1 → results 1-10
max_results: 10, page: 2 → results 11-20`,
minimum: 1,
maximum: 20,
default: 10
},
page: {
type: "number",
description: `Which page of results to retrieve (1-based).
• Default: 1 (first page)
• Use this to paginate through large result sets
• Example response will show: {found: 50, page: 1, per_page: 10, has_more: true}
→ This means 50 total results exist, showing results 1-10, more pages available
→ Set page: 2 to get results 11-20
→ Set page: 3 to get results 21-30, etc.
• Continue incrementing until has_more: false`,
minimum: 1,
default: 1
}
},
required: [],
additionalProperties: true
},
handler: async (args) => {
try {
// Defaults & basic params
const query = args.query || "*";
const search_type = args.search_type || "hybrid";
const sort_by_arg = args.sort_by || "relevance";
const max_results = Math.max(1, Math.min(args.max_results || 10, 20));
const page = Math.max(1, args.page || 1);
// Reconstruct filters object from flattened parameters
const filters = {
imo: args.imo,
vessel_name: args.vessel_name,
category: args.category,
importance: args.importance,
from_date: args.from_date,
to_date: args.to_date
};
console.log(`[smart_casefile_search] q='${query}' type=${search_type} filters=${JSON.stringify(filters)} sort=${sort_by_arg} limit=${max_results} page=${page}`);
// 🚨 CRITICAL: Input Validation (Before Production)
// 1. Enum Validation - Prevent silent failures
const validCategories = [
"General",
"LocationAndCargoActivity",
"sire",
"loReport",
"internalAudit",
"vir",
"FleetAlert",
"SmartShipAlert",
"fleetAlert",
"smartShipAlert",
"locationAndCargoActivity",
"classSurveyAndCertificateStatus"
];
const validImportance = ["Low", "Medium", "High"];
if (filters.category && !validCategories.includes(filters.category)) {
return [{
type: "text",
text: `Invalid category '${filters.category}'. Valid categories: ${validCategories.join(', ')}`
}];
}
if (filters.importance && !validImportance.includes(filters.importance)) {
return [{
type: "text",
text: `Invalid importance '${filters.importance}'. Valid values: ${validImportance.join(', ')}`
}];
}
// 2. Date Validation - Prevent server crashes
const validateAndParseDate = (dateStr, fieldName) => {
// Check basic format first
if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dateStr)) {
throw new Error(`Invalid ${fieldName} format '${dateStr}'. Please use dd/mm/yyyy format (e.g., 15/03/2024).`);
}
const [day, month, year] = dateStr.split('/').map(Number);
// Validate ranges
if (day < 1 || day > 31) {
throw new Error(`Invalid day in ${fieldName}: '${day}'. Day must be between 1-31.`);
}
if (month < 1 || month > 12) {
throw new Error(`Invalid month in ${fieldName}: '${month}'. Month must be between 1-12.`);
}
if (year < 1900 || year > 2100) {
throw new Error(`Invalid year in ${fieldName}: '${year}'. Year must be between 1900-2100.`);
}
// Create date and check if it's valid
const date = new Date(year, month - 1, day);
if (date.getDate() !== day || date.getMonth() !== month - 1 || date.getFullYear() !== year) {
throw new Error(`Invalid date in ${fieldName}: '${dateStr}' is not a valid calendar date.`);
}
const timestamp = Math.floor(date.getTime() / 1000);
if (isNaN(timestamp)) {
throw new Error(`Invalid ${fieldName} format '${dateStr}'. Please use dd/mm/yyyy format.`);
}
return timestamp;
};
// Validate and parse dates once
let fromTimestamp = null;
let toTimestamp = null;
if (filters.from_date) {
try {
fromTimestamp = validateAndParseDate(filters.from_date, "from_date");
}
catch (error) {
return [{
type: "text",
text: error instanceof Error ? error.message : `Invalid from_date format '${filters.from_date}'. Please use dd/mm/yyyy format.`
}];
}
}
if (filters.to_date) {
try {
toTimestamp = validateAndParseDate(filters.to_date, "to_date");
}
catch (error) {
return [{
type: "text",
text: error instanceof Error ? error.message : `Invalid to_date format '${filters.to_date}'. Please use dd/mm/yyyy format.`
}];
}
}
// 🚨 END CRITICAL VALIDATION
// Resolve vessel name → IMO (if required)
let imo = filters.imo;
let vesselNameFilter = null;
if (!imo && filters.vessel_name) {
try {
const vessel_data = await getVesselDetails(filters.vessel_name);
if (vessel_data && vessel_data.imo) {
imo = vessel_data.imo;
console.log(`[smart_casefile_search] Successfully resolved vessel '${filters.vessel_name}' to IMO: ${imo}`);
}
else {
// IMO not found in response, use vessel name as fallback
vesselNameFilter = filters.vessel_name;
console.log(`[smart_casefile_search] No IMO found for vessel '${filters.vessel_name}', using vessel name as filter`);
}
}
catch (error) {
// Error occurred, use vessel name as fallback
vesselNameFilter = filters.vessel_name;
console.log(`[smart_casefile_search] Error resolving vessel '${filters.vessel_name}': ${error}. Using vessel name as filter`);
}
}
// Build Typesense `filter_by` string
const fb = [];
if (imo) {
fb.push(`imo:=${imo}`);
}
else if (vesselNameFilter) {
fb.push(`vesselName:='${vesselNameFilter}'`);
}
if (filters.category) {
fb.push(`category:='${filters.category}'`);
}
if (filters.importance) {
fb.push(`importance:='${filters.importance}'`);
}
// Date range filtering - using lastCasefileUpdateDate
if (fromTimestamp) {
fb.push(`lastCasefileUpdateDate:>=${fromTimestamp}`);
}
if (toTimestamp) {
// Add 86399 seconds (23:59:59) to include the entire end date
fb.push(`lastCasefileUpdateDate:<=${toTimestamp + 86399}`);
}
const filter_by = fb.length > 0 ? fb.join(" && ") : undefined;
// Prepare search payload
const query_by = search_type === "semantic"
? "embedding"
: "embedding,embedding_text,vesselName";
// Fix the sort configuration
const sort_map = {
"recent": "lastCasefileUpdateDate:desc",
"oldest": "lastCasefileUpdateDate:asc"
};
const payload = {
q: query,
query_by: query_by,
include_fields: "casefileInitiationDate,vesselName,_id,lastCasefileUpdateDate,casefile,summary,link,category,importance,imo",
exclude_fields: "embedding",
per_page: max_results,
page: page,
prefix: false
};
// Only add sort_by if not using relevance (let Typesense use default search score sorting)
if (sort_by_arg !== "relevance") {
const sort_by = sort_map[sort_by_arg] || sort_map["recent"];
payload.sort_by = sort_by;
}
if (filter_by) {
payload.filter_by = filter_by;
}
// Execute search
const typesenseClient = getTypesenseClient(config);
const results = await typesenseClient.collections("emailCasefile").documents().search(payload);
// Post-process hits
const hits = results.hits || [];
const processed_hits = [];
for (const h of hits) {
const doc = convertCasefileDates(h.document);
processed_hits.push({
id: doc._id,
score: h.text_match || 0,
document: doc,
});
}
// Apply IMO filtering to hits before generating artifacts
const { isValidImoForCompany, shouldBypassImoFiltering } = await import('../utils/imoUtils.js');
const filteredHits = processed_hits.filter(hit => {
const companyName = config.companyName || '';
// Skip filtering for admin companies
if (shouldBypassImoFiltering(companyName)) {
return true;
}
// Check if hit has IMO and validate it
const document = hit.document;
if (document.imo) {
return isValidImoForCompany(document.imo);
}
return true; // Keep hits without IMO
});
const total_found = results.found || 0;
const has_more = (page * max_results) < total_found;
// Generate informative message if all hits were filtered out
let response_obj = {
found: filteredHits.length, // Use filtered count
page: page,
per_page: max_results,
has_more: has_more,
hits: filteredHits, // Use filtered hits
};
// If we had results but they were all filtered out, add informative message
if (processed_hits.length > 0 && filteredHits.length === 0) {
const companyName = config.companyName || 'current company';
// Get the IMO numbers that were filtered out
const filteredImos = processed_hits
.map(hit => hit.document.imo)
.filter(imo => imo && !isValidImoForCompany(imo))
.map(imo => String(imo));
const uniqueImos = [...new Set(filteredImos)];
let message;
if (uniqueImos.length === 1) {
message = `Vessel with this IMO ${uniqueImos[0]} is not part of ${companyName}`;
}
else if (uniqueImos.length > 1) {
message = `Vessels with IMO ${uniqueImos.join(', ')} are not part of ${companyName}`;
}
else {
message = `No vessels found that belong to ${companyName}`;
}
response_obj.message = message;
}
// Build artifacts from filtered hits (for your browser-view visualizations)
const link_data = filteredHits.map(h => ({
title: h.document.casefile,
url: h.document.link
})).filter(item => item.url);
const artifacts = getListOfArtifacts(query !== "*" ? query : "smart search", link_data);
return [
{
type: "text",
title: `Smart Casefile Search Results (page ${page})`,
text: JSON.stringify(response_obj, null, 2),
format: "json",
},
...artifacts,
];
}
catch (error) {
throw new Error(`Error performing smart casefile search: ${error}`);
}
}
},
{
name: 'email_search',
description: [
'Use this tool to retrieve one or more emails from the company\'s email archive based on flexible search criteria.',
'It can answer queries like "Get all emails for NEXUS VICTORIA",',
'"Get all emails from shipping@bothwinnb.com in the last 3 days",',
'"Get all emails to Captain John", "Get all emails cc\'d to Alex Lee",',
'"Get all emails about pilot boarding", or any combination of these parameters.',
'You can provide one or more query parameters to refine your search, including vessel name, IMO, sender/recipient/cc name or email, tags, subject, date range, attachments, and more.',
'Multiple matching emails may be returned for a single query.'
].join(' '),
inputSchema: {
type: 'object',
properties: {
vessel_name: {
type: 'string',
description: "Vessel name to filter emails. Partial or full vessel names are accepted."
},
vessel_id: {
type: 'string',
description: "Vessel ID to filter emails for a specific vessel."
},
imo: {
type: 'string',
description: "IMO number of the vessel."
},
from_email: {
type: 'string',
description: "Sender's email address. Use to get all emails sent by this address."
},
from_name: {
type: 'string',
description: "Sender's name. Use to get all emails sent by this person (partial or full name accepted)."
},
to_email: {
type: 'string',
description: "Recipient's email address. Use to get all emails sent to this address."
},
to_name: {
type: 'string',
description: "Recipient's name. Use to get all emails sent to this person (partial or full name accepted)."
},
cc_email: {
type: 'string',
description: "CC recipient's email address. Use to get all emails where this address is CC'd."
},
cc_name: {
type: 'string',
description: "CC recipient's name. Use to get all emails where this person is CC'd (partial or full name accepted)."
},
subject_query: {
type: 'string',
description: "Search for specific words or phrases in the email subject."
},
casefile_query: {
type: 'string',
description: "Search for specific terms in the casefile field (e.g., terminal info, agent contact)."
},
narrative_query: {
type: 'string',
description: "Search for specific text in the narrative field of the email."
},
tags: {
type: 'array',
items: { type: 'string' },
description: "Filter emails by one or more tags (e.g., agent, crewlist)."
},
importance: {
type: 'string',
description: "Importance of the email (e.g., 'high', 'low')."
},
date_from: {
type: 'string',
format: 'date-time',
description: "Start date (inclusive) in ISO8601 format. Used to filter emails by earliest date."
},
date_to: {
type: 'string',
format: 'date-time',
description: "End date (inclusive) in ISO8601 format. Used to filter emails by latest date."
},
has_attachments: {
type: 'boolean',
description: "If true, only emails with one or more attachments are returned."
},
limit: {
type: 'integer',
description: "Maximum number of emails to return (default 50, max 200)."
},
sort_by_date: {
type: 'string',
enum: ['asc', 'desc'],
description: "Sort results by date in ascending ('asc') or descending ('desc', default) order."
}
}
},
handler: async (args) => {
try {
const limit = Math.max(1, Math.min(args.limit || 50, 200));
const sort_by_date = args.sort_by_date || 'desc';
console.log(`[email_search] Searching emails with filters: ${JSON.stringify(args)}`);
// Build Typesense search query
const query_parts = [];
const filter_parts = [];
// use a loop to add all the query_parts to the query
const list_of_query_fields_from_args = ['vessel_name', 'vessel_id', 'imo', 'from_email', 'from_name', 'to_email', 'to_name', 'cc_email', 'cc_name', 'subject_query', 'casefile_query', 'narrative_query', 'tags', 'importance', 'date_from', 'date_to', 'has_attachments', 'limit', 'sort_by_date'];
for (const part of list_of_query_fields_from_args) {
if (args[part]) {
query_parts.push(args[part]);
}
}
const query = query_parts.length > 0 ? query_parts.join(' ') : '*';
// Build filters - mapping to vessel_mails schema
// IMO filter
if (args.imo) {
// imo is a string in the schema, not a number
filter_parts.push(`imo:=${args.imo}`);
}
// Date filters - using date_time field which is int64 in the schema
if (args.date_from) {
const from_timestamp = Math.floor(new Date(args.date_from).getTime());
filter_parts.push(`date_time:>=${from_timestamp}`);
}
if (args.date_to) {
const to_timestamp = Math.floor(new Date(args.date_to).getTime());
filter_parts.push(`date_time:<=${to_timestamp}`);
}
const filter_by = filter_parts.length > 0 ? filter_parts.join(' && ') : undefined;
// Prepare search payload - using fields from the schema
const payload = {
q: query,
query_by: 'casefile,cc_recipients_email_addresses,subject,narrative,sender_name,sender_email_address,to_recipients_names,to_recipients_email_addresses,cc_recipients_names,vessel_name,tags',
include_fields: 'mail_body_id,subject,vessel_name,imo,vessel_id,sender_name,sender_email_address,to_recipients_names,to_recipients_email_addresses,cc_recipients_names,cc_recipients_email_addresses,narrative,casefile,attachments,date_time,tags,mail_body_link',
per_page: limit,
page: 1,
sort_by: `date_time:${sort_by_date}`,
prefix: false
};
if (filter_by) {
payload.filter_by = filter_by;
}
// Execute search on vessel_mails collection
const typesenseClient = getTypesenseClient(config);
const results = await typesenseClient.collections("vessel_mails").documents().search(payload);
// Process results
const hits = results.hits || [];
// Format Typesense results with date formatting
const processed_hits = hits.map((hit) => ({
id: hit.document._id,
score: hit.text_match || 0,
document: {
...hit.document,
date: hit.document.date_time ? new Date((hit.document.date_time)).toISOString().slice(0, 19).replace('T', ' ') : null
}
}));
// Apply IMO filtering to hits before generating artifacts
const { isValidImoForCompany, shouldBypassImoFiltering } = await import('../utils/imoUtils.js');
const filteredHits = processed_hits.filter(hit => {
const companyName = config.companyName || '';
// Skip filtering for admin companies
if (shouldBypassImoFiltering(companyName)) {
return true;
}
// Check if hit has IMO and validate it
const document = hit.document;
if (document.imo) {
return isValidImoForCompany(document.imo);
}
return true; // Keep hits without IMO
});
// Collect email dump links for artifacts from filtered hits
const emailArtifactLinks = [];
// Check each filtered hit for mail_body_link directly from Typesense results
for (const hit of filteredHits) {
const document = hit.document;
// Only add to links if mail_body_link is present in the Typesense document
if (document && document.mail_body_link) {
emailArtifactLinks.push({
title: document.subject || `Email ${document._id}`,
url: document.mail_body_link
});
}
}
// Generate informative message if all hits were filtered out
let response_obj = {
found: filteredHits.length, // Use filtered count
hits: filteredHits // Use filtered hits
};
// If we had results but they were all filtered out, add informative message
if (processed_hits.length > 0 && filteredHits.length === 0) {
const companyName = config.companyName || 'current company';
// Get the IMO numbers that were filtered out
const filteredImos = processed_hits
.map(hit => {
const doc = hit.document;
return doc.imo;
})
.filter(imo => imo && !isValidImoForCompany(imo))
.map(imo => String(imo));
const uniqueImos = [...new Set(filteredImos)];
let message;
if (uniqueImos.length === 1) {
message = `Vessel with this IMO ${uniqueImos[0]} is not part of ${companyName}`;
}
else if (uniqueImos.length > 1) {
message = `Vessels with IMO ${uniqueImos.join(', ')} are not part of ${companyName}`;
}
else {
message = `No vessels found that belong to ${companyName}`;
}
response_obj.message = message;
}
// Return Typesense results as main content and email as artifacts
const response = [{
type: "text",
title: `Email Search Results (${response_obj.found} found)`,
text: JSON.stringify(response_obj, null, 2),
format: "json"
}];
// Generate artifacts using the provided function
const artifacts = getListOfArtifacts("email_search", emailArtifactLinks);
// Add artifacts if any were found
return [...response, ...artifacts];
}
catch (error) {
console.error(`Error searching emails: ${error}`);
return [{
type: "text",
title: "Email Search Error",
text: `Failed to search emails: ${error instanceof Error ? error.message : String(error)}`,
format: "text"
}];
}
}
},
{
name: "get_email_details",
description: "Retrieve the complete details of an email by its ID. This tool fetches the full email content, including body, attachments, and metadata from MongoDB.",
inputSchema: {
type: "object",
properties: {
mail_body_id: {
type: "string",
description: "The MongoDB Object ID of the email to retrieve. This is typically obtained from email_search results."
}
},
required: ["mail_body_id"]
},
handler: async (args) => {
try {
const emailId = args.mail_body_id;
if (!emailId) {
throw new Error("mail_body_id is required");
}
logger.info(`[get_email_details] Retrieving email details for ID: ${emailId}`);
const result = await get_full_email_details(emailId);
if (!result.found) {
return [{
type: "text",
title: "Email Not Found",
text: result.message,
format: "text"
}];
}
return [{
type: "text",
title: `Email Details (ID: ${emailId})`,
text: JSON.stringify(result, null, 2),
format: "json"
}];
}
catch (error) {
logger.error(`Error retrieving email details: ${error}`);
return [{
type: "text",
title: "Email Retrieval Error",
text: `Failed to retrieve email details: ${error instanceof Error ? error.message : String(error)}`,
format: "text"
}];
}
}
},
{
name: "write_casefile_data",
description: ("Creates or updates casefile-related data. " +
"Supports two distinct operations:\n" +
"- write_casefile: Create or update casefile metadata (e.g., summary, title, importance).\n" +
"- write_page: Add or update a page under an existing casefile, including content and indexing." +
"Only pass arguments explicitly required or allowed for the chosen operation."),
inputSchema: {
type: "object",
properties: {
operation: {
type: "string",
enum: ["write_casefile", "write_page"],
description: ("Specifies the writing operation: 'write_casefile' for creating new casefile or 'write_page' for page content of already existing casefile.")
},
casefile_url: {
type: "string",
description: ("The unique identifier of the casefile, direct casefile url link." +
"Required for 'write_page'.")
},
casefileName: {
type: "string",
enum: ["Charter Party"],
description: ("Required for 'write_casefile'. Name of the casefile")
},
category: {
type: "string",
enum: ["charterParty"],
description: ("Required for 'write_casefile' . Category of the casefile")
},
currentStatus: {
type: "string",
description: ("<review the casefile and plan to create current status in one line, highlighting keywords>" +
"Required for 'write_casefile': Current status of the casefile, it will be of 4-5 words." +
"Required for 'write_page': update or kept it same status of the casefile based on recent received email. it willbe of 4-5 words.")
},
casefileSummary: {
type: "string",
description: ("Required for 'write_casefile'. Summary or high-level description of the casefile.\n" +
"Optional for 'write_page': can provide updated summary if needed.")
},
importance: {
type: "number",
minimum: 0,
maximum: 100,
description: ("It will show the importance of the casefile reference for the urgency and importance of the matter in the casefile." +
"Required for 'write_casefile'. Importance score of the casefile (0–100).\n" +
"required for 'write_page': can provide an updated score based on the new email content added to the casefile.")
},
imo: {
type: "integer",
description: ("Required for 'write_casefile'. IMO number of the associated vessel.")
},
role: {
type: "string",
enum: ["incident", "legal", "regulatory", "other"],
description: ("Required for 'write_casefile'. Role/category of the casefile.")
},
summary: {
type: "string",
description: ("Required for 'write_page'. Detailed content or summary of the new page.")
},
topic: {
type: "string",
description: ("Required for 'write_page'.It is of 4-8 words aboyt what this document is about.")
},
facts: {
type: "string",
description: ("Required for 'write_page'..It will have the highlighted facts/information from the database.")
},
detailed_report: {
type: "string",
description: ("Required for 'write_page'. It will have the detailed report of the casefile in markdown format.")
},
links: {
type: "array",
items: {
type: "string"
},
description: ("Required for 'write_page'. Relevent links you want to add to the case file.")
}
},
required: ["operation"],
additionalProperties: false
},
handler: async (args) => {
const result = await write_casefile_data(args);
return [result];
}
},
// {
// name: "update_casefile",
// description: "General-purpose tool to update editable fields in an existing casefile. Supports both direct field updates and individual page/index operations (add, delete, edit). Note: Some fields like imo, vesselName, vesselId, createdAt, updatedAt are protected and cannot be modified.",
// inputSchema: {
// type: "object",
// properties: {
// casefile_id: {
// type: "string",
// description: "The MongoDB ObjectId of the casefile to update. Must be a valid ObjectId format."
// },
// updates: {
// type: "object",
// description: "Object containing the field names and their new values to update. Supports: 1) Direct field updates: casefile, currentStatus, status, plan_status, importance, originalImportance, maxImportance, conversationTopic, summary, category, tag, isClone, importanceReason, ceoLevelProblemReason, role, casefilePlans, followUp, taskAssigned, index, pages, etc. 2) Individual operations: add_index (object), add_page (object), delete_index (pagenum), delete_page (pagenum), edit_index ({pagenum, ...fields}), edit_page ({pagenum, ...fields}). Protected fields (imo, vesselName, vesselId, createdAt, updatedAt, _id, link, weblinkKey) will be ignored if provided. Cannot mix direct updates with individual operations.",
// additionalProperties: true,
// minProperties: 1
// }
// },
// required: ["casefile_id", "updates"],
// additionalProperties: false
// },
// handler: async (args: {
// casefile_id: string;
// updates: Record<string, any>;
// }): Promise<Array<TextContent | ImageContent>> => {
// try {
// const { casefile_id, updates } = args;
// // Validate casefile_id
// if (!casefile_id || !ObjectId.isValid(casefile_id)) {
// throw new Error("Valid casefile_id (MongoDB ObjectId) is required");
// }
// // Validate updates object
// if (!updates || typeof updates !== 'object' || Object.keys(updates).length === 0) {
// throw new Error("Updates object with at least one field is required");
// }
// console.log(`[update_casefile] Updating casefile ${casefile_id} with fields: ${Object.keys(updates).join(', ')}`);
// const mongoClient = await getMongoClient(config.mongoUri);
// const db = mongoClient.db(config.dbName);
// const collection = db.collection("casefiles");
// // Check if casefile exists
// const existingCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
// if (!existingCasefile) {
// return [{
// type: "text" as const,
// text: `No casefile found with ID: ${casefile_id}`
// }];
// }
// // Define protected fields that cannot be updated
// const protectedFields = [
// '_id', 'imo', 'vesselName', 'vesselId', 'createdAt', 'updatedAt',
// 'link', 'weblinkKey', 'casefileMessageId'
// ];
// // Filter out protected fields and warn about them
// const filteredUpdates: Record<string, any> = {};
// const ignoredFields: string[] = [];
// for (const [key, value] of Object.entries(updates)) {
// if (protectedFields.includes(key)) {
// ignoredFields.push(key);
// } else {
// filteredUpdates[key] = value;
// }
// }
// if (Object.keys(filteredUpdates).length === 0) {
// return [{
// type: "text" as const,
// text: `No editable fields provided. Protected fields (${ignoredFields.join(', ')}) cannot be updated.`
// }];
// }
// // Handle special operations for index and pages
// const hasIndexOps = 'add_index' in filteredUpdates || 'delete_index' in filteredUpdates || 'edit_index' in filteredUpdates;
// const hasPageOps = 'add_page' in filteredUpdates || 'delete_page' in filteredUpdates || 'edit_page' in filteredUpdates;
// const hasDirectUpdate = 'index' in filteredUpdates || 'pages' in filteredUpdates;
// if (hasDirectUpdate && (hasIndexOps || hasPageOps)) {
// return [{
// type: "text" as const,
// text: "Error: Cannot mix direct 'index'/'pages' updates with individual operations (add_index, delete_index, edit_index, add_page, delete_page, edit_page)."
// }];
// }
// // If both index and pages are provided directly, validate they have matching lengths
// if ('index' in filteredUpdates && 'pages' in filteredUpdates) {
// const indexArray = Array.isArray(filteredUpdates.index) ? filteredUpdates.index : [];
// const pagesArray = Array.isArray(filteredUpdates.pages) ? filteredUpdates.pages : [];
// if (indexArray.length !== pagesArray.length) {
// return [{
// type: "text" as const,
// text: `Error: 'index' and 'pages' arrays must have the same length. Found index: ${indexArray.length}, pages: ${pagesArray.length}`
// }];
// }
// }
// // Handle individual page/index operations
// let mongoUpdates: any = {};
// const specialOps = ['add_index', 'delete_index', 'edit_index', 'add_page', 'delete_page', 'edit_page'];
// if (hasIndexOps || hasPageOps) {
// // Process individual operations using MongoDB array operators
// for (const [key, value] of Object.entries(filteredUpdates)) {
// if (specialOps.includes(key)) {
// switch (key) {
// case 'add_index':
// if (value && typeof value === 'object') {
// // Calculate next pagenum
// const maxPageNum = Math.max(
// ...(existingCasefile.index || []).map((item: any) => item.pagenum || 0),
// ...(existingCasefile.pages || []).map((item: any) => item.pagenum || 0),
// -1
// );
// const newPageNum = maxPageNum + 1;
// const indexItem = { ...value, pagenum: newPageNum };
// if (indexItem.referenceId && typeof indexItem.referenceId === 'string' && ObjectId.isValid(indexItem.referenceId)) {
// indexItem.referenceId = new ObjectId(indexItem.referenceId);
// }
// mongoUpdates.$push = { ...mongoUpdates.$push, index: indexItem };
// }
// break;
// case 'add_page':
// if (value && typeof value === 'object') {
// // Calculate next pagenum (should match add_index)
// const maxPageNum = Math.max(
// ...(existingCasefile.index || []).map((item: any) => item.pagenum || 0),
// ...(existingCasefile.pages || []).map((item: any) => item.pagenum || 0),
// -1
// );
// const newPageNum = maxPageNum + 1;
// const pageItem = { ...value, pagenum: newPageNum };
// if (pageItem.referenceId && typeof pageItem.referenceId === 'string' && ObjectId.isValid(pageItem.referenceId)) {
// pageItem.referenceId = new ObjectId(pageItem.referenceId);
// }
// mongoUpdates.$push = { ...mongoUpdates.$push, pages: pageItem };
// }
// break;
// case 'delete_index':
// if (typeof value === 'number') {
// mongoUpdates.$pull = { ...mongoUpdates.$pull, index: { pagenum: value } };
// }
// break;
// case 'delete_page':
// if (typeof value === 'number') {
// mongoUpdates.$pull = { ...mongoUpdates.$pull, pages: { pagenum: value } };
// }
// break;
// case 'edit_index':
// if (value && typeof value === 'object' && 'pagenum' in value) {
// const { pagenum, ...updateFields } = value;
// if (updateFields.referenceId && typeof updateFields.referenceId === 'string' && ObjectId.isValid(updateFields.referenceId)) {
// updateFields.referenceId = new ObjectId(updateFields.referenceId);
// }
// mongoUpdates.$set = { ...mongoUpdates.$set };
// for (const [field, val] of Object.entries(updateFields)) {
// mongoUpdates.$set[`index.$[elem].${field}`] = val;
// }
// mongoUpdates.arrayFilters = mongoUpdates.arrayFilters || [];
// mongoUpdates.arrayFilters.push({ "elem.pagenum": pagenum });
// }
// break;
// case 'edit_page':
// if (value && typeof value === 'object' && 'pagenum' in value) {
// const { pagenum, ...updateFields } = value;
// if (updateFields.referenceId && typeof updateFields.referenceId === 'string' && ObjectId.isValid(updateFields.referenceId)) {
// updateFields.referenceId = new ObjectId(updateFields.referenceId);
// }
// mongoUpdates.$set = { ...mongoUpdates.$set };
// for (const [field, val] of Object.entries(updateFields)) {
// mongoUpdates.$set[`pages.$[elem].${field}`] = val;
// }
// mongoUpdates.arrayFilters = mongoUpdates.arrayFilters || [];
// mongoUpdates.arrayFilters.push({ "elem.pagenum": pagenum });
// }
// break;
// }
// }
// }
// // Add updatedAt to $set
// mongoUpdates.$set = { ...mongoUpdates.$set, updatedAt: new Date() };
// } else {
// // Prepare standard update object with automatic updatedAt timestamp
// const standardUpdates = { ...filteredUpdates };
// // Remove special operations from standard updates
// for (const op of specialOps) {
// delete standardUpdates[op];
// }
// mongoUpdates = {
// $set: {
// ...standardUpdates,
// updatedAt: new Date()
// }
// };
// }
// // Handle array fields for standard updates that might contain ObjectIds
// if (mongoUpdates.$set) {
// if (mongoUpdates.$set.index && Array.isArray(mongoUpdates.$set.index)) {
// mongoUpdates.$set.index = mongoUpdates.$set.index.map((item: any) => {
// if (item.referenceId && typeof item.referenceId === 'string' && ObjectId.isValid(item.referenceId)) {
// return { ...item, referenceId: new ObjectId(item.referenceId) };
// }
// return item;
// });
// }
// if (mongoUpdates.$set.pages && Array.isArray(mongoUpdates.$set.pages)) {
// mongoUpdates.$set.pages = mongoUpdates.$set.pages.map((page: any) => {
// if (page.referenceId && typeof page.referenceId === 'string' && ObjectId.isValid(page.referenceId)) {
// return { ...page, referenceId: new ObjectId(page.referenceId) };
// }
// return page;
// });
// }
// }
// // Execute the update with appropriate options
// const updateOptions: any = { upsert: false };
// if (mongoUpdates.arrayFilters && mongoUpdates.arrayFilters.length > 0) {
// updateOptions.arrayFilters = mongoUpdates.arrayFilters;
// }
// const result = await collection.updateOne(
// { _id: new ObjectId(casefile_id) },
// mongoUpdates,
// updateOptions
// );
// if (result.matchedCount === 0) {
// return [{
// type: "text" as const,
// text: `No casefile found with ID: ${casefile_id}`
// }];
// }
// if (result.modifiedCount === 0) {
// return [{
// type: "text" as const,
// text: `Casefile ${casefile_id} found but no changes were made (values may be identical)`
// }];
// }
// // Fetch the updated document
// const updatedCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
// if (!updatedCasefile) {
// throw new Error(`Failed to retrieve updated casefile ${casefile_id}`);
// }
// // Generate new link with updated casefile data
// let newLink = updatedCasefile.link;
// try {
// newLink = await generateCasefileWeblink(casefile_id);
// // Update the link in the database
// await collection.updateOne(
// { _id: new ObjectId(casefile_id) },
// { $set: { link: newLink } }
// );
// console.log(`[update_casefile] Generated new link for casefile ${casefile_id}: ${newLink}`);
// } catch (error) {
// console.error(`[update_casefile] Failed to generate new link for casefile ${casefile_id}: ${error}`);
// // Continue with existing link - don't fail the entire operation
// }
// // Fetch the final updated document with new link for sync
// const finalCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
// if (finalCasefile) {
// try {
// await pushToTypesense(finalCasefile, 'upsert');
// console.log(`[update_casefile] Successfully synced casefile ${casefile_id}`);
// } catch (error) {
// console.error(`[update_casefile] Sync error for casefile ${casefile_id}: ${error}`);
// }
// }
// const response = {
// casefile_id: casefile_id,
// updated_fields: Object.keys(filteredUpdates),
// modified_count: result.modifiedCount,
// removed_protected_fields: ignoredFields.length > 0 ? ignoredFields : undefined,
// auto_injected: ["updatedAt", "link"],
// updated_at: new Date().toISOString(),
// new_link: newLink
// };
// return [{
// type: "text" as const,
// title: `Casefile Updated: ${casefile_id}`,
// text: JSON.stringify(response, null, 2),
// format: "json"
// }];
// } catch (error) {
// console.error(`[update_casefile] Error: ${error}`);
// return [{
// type: "text" as const,
// title: "Casefile Update Error",
// text: `Failed to update casefile: ${error instanceof Error ? error.message : String(error)}`,
// format: "text"
// }];
// }
// }
// },
{
name: "update_casefile",
description: `Advanced casefile update tool using raw MongoDB update queries. Provides maximum flexibility with built-in validation and safety checks. Supports all MongoDB update operators while protecting critical fields and maintaining data consistency.
## CASEFILE SCHEMA STRUCTURE:
### Core Fields (Editable):
- casefile: string - Title/name of the casefile (e.g., "🛢️ LUBE OIL ANALYSIS MONITORING - OCEAN BREEZER")
- currentStatus: string - Current status description (e.g., "Critical Issues Identified - **Steering Gear Water Contamination**")
- status: string - Status code (e.g., "actionRequired", "completed", "pending")
- plan_status: string - Plan status (e.g., "plan_created", "unprocessed", "processed")
- importance: number - Importance score 0-100 (e.g., 85)
- originalImportance: number - Initial importance score
- maxImportance: number - Maximum importance reached
- conversationTopic: string[] - Array of conversation topics
- summary: string - Detailed markdown summary with executive information
- category: string - Category type (e.g., "loReport", "sire", "General")
- tag: string[] - Array of tags for categorization
- isClone: boolean - Whether this is a cloned casefile
- importanceReason: string - Explanation of importance level
- ceoLevelProblemReason: string - CEO escalation reasoning
- role: string[] - Array of roles/responsibilities
- followUp: string - Next follow-up action description
### Array Fields (Complex Structures):
#### index: Array of index entries
Structure: {
type: string (e.g., "email", "SIYA"),
referenceId: ObjectId - Reference to related document,
topic: string - Topic/title of the entry,
pagenum: number - Page number (auto-calculated if not provided),
dateTime: Date - Timestamp,
plan_status: string - Processing status
}
#### pages: Array of page entries (must match index array length)
Structure: {
referenceId: ObjectId - Must match corresponding index entry,
summary: string - Detailed markdown content,
dateTime: Date - Timestamp,
toRecipientsEmailAddresses: string - Email recipients,
senderEmailAddress: string - Sender email,
senderName: string - Sender name,
subject: string - Email/document subject,
flag: string - A markdown text indicating if it is a serious issue , normal , informaiton ,mail etc. Add visual symbols to the markdonw text
type: string - Type (email, SIYA, etc.),
link: string - URL to document/email,
attachments: string[] - Array of attachment URLs,
tags: string[] - Array of tags,
plan_status: string - Processing status,
pagenum: number - Page number (must match index)
content: It is markdown format text which is 2 lint gist of the summary field text
}
#### casefilePlans: Array of plan objects
Structure: {
dateTime: Date - Plan creation date,
flag: string - Plan title/flag,
plan: string - Detailed markdown plan content,
color: string - UI color code,
content: string - Brief plan description
}
#### taskAssigned: Array of task objects
Structure: {
id: string - Task identifier,
task: string - Task description,
status: string - Task status (pending, completed, in_progress),
dependency: string[] - Array of dependent task IDs,
taskDate: Date - Task due date,
isUpdated: boolean - Whether task was updated,
task_update_tracker: string - Update tracking info,
assignee: string - Assigned person code
}
### Protected Fields (Auto-managed, cannot be updated):
- _id, imo, vesselName, vesselId, createdAt, updatedAt, link, weblinkKey, casefileMessageId
## MONGODB UPDATE EXAMPLES:
### Update basic fields:
{"$set": {"currentStatus": "Investigation Complete", "importance": 90, "status": "completed"}}
### Add new page and index (must be done together):
{"$push": {"index": {"type": "email", "referenceId": "686590168052c04f5b057069", "topic": "New findings"}, "pages": {"referenceId": "686590168052c04f5b057069", "summary": "Detailed analysis", "type": "email"}}}
### Update specific array elements by pagenum:
{"$set": {"pages.$[elem].plan_status": "completed"}, array_filters: [{"elem.pagenum": 2}]}
### Remove entries by pagenum:
{"$pull": {"index": {"pagenum": 3}, "pages": {"pagenum": 3}}}
### Add to arrays:
{"$addToSet": {"conversationTopic": "New topic", "tag": "urgent"}}
## VALIDATION RULES:
- **Flag, Content, Summary Dependency**: When updating any of the fields 'flag', 'content', or 'summary' (in pages or casefilePlans arrays), all three fields must be updated together in the same operation.`,
inputSchema: {
type: "object",
properties: {
casefile_id: {
type: "string",
description: "The MongoDB ObjectId of the casefile to update. Must be a valid ObjectId format."
},
mongodb_update: {
type: "object",
description: "MongoDB update query object supporting all MongoDB update operators ($set, $push, $pull, $addToSet, $unset, $inc, etc.). Example: {\"$set\": {\"currentStatus\": \"Updated\", \"importance\": 75}, \"$push\": {\"pages\": {\"summary\": \"New content\", \"type\": \"email\"}}, \"$pull\": {\"index\": {\"pagenum\": 5}}}. Protected fields (imo, vesselName, vesselId, createdAt, _id, link, weblinkKey, casefileMessageId) will be automatically filtered out. The system will auto-inject updatedAt timestamp and handle ObjectId conversions.",
additionalProperties: true,
minProperties: 1
},
array_filters: {
type: "array",
description: "MongoDB array filters for positional updates. Example: [{\"elem.pagenum\": 3}] to update specific array elements. Used with operators like $set on nested array fields (e.g., \"pages.$[elem].summary\").",
items: { type: "object" }
}
},
required: ["casefile_id", "mongodb_update"],
additionalProperties: false
},
handler: async (args) => {
try {
const { casefile_id, mongodb_update, array_filters } = args;
// Validate casefile_id
if (!casefile_id || !ObjectId.isValid(casefile_id)) {
throw new Error("Valid casefile_id (MongoDB ObjectId) is required");
}
// Validate mongodb_update
if (!mongodb_update || typeof mongodb_update !== 'object' || Object.keys(mongodb_update).length === 0) {
throw new Error("mongodb_update object with at least one operation is required");
}
console.log(`[update_casefile] Updating casefile ${casefile_id} with MongoDB query:`, JSON.stringify(mongodb_update));
const mongoClient = await getMongoClient(config.mongoUri);
const db = mongoClient.db(config.dbName);
const collection = db.collection("casefiles");
// Check if casefile exists
const existingCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
if (!existingCasefile) {
return [{
type: "text",
text: `No casefile found with ID: ${casefile_id}`
}];
}
// Define protected fields that cannot be updated
const protectedFields = [
'_id', 'imo', 'vesselName', 'vesselId', 'createdAt', 'updatedAt',
'link', 'weblinkKey', 'casefileMessageId'
];
// Helper function to extract all field paths from a query object
const extractFieldPaths = (obj, prefix = '') => {
const paths = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (key.startsWith('$')) {
// Handle MongoDB operators
if (key === '$set' || key === '$unset' || key === '$inc' || key === '$mul') {
if (typeof value === 'object' && value !== null) {
for (const field of Object.keys(value)) {
paths.push(field);
}
}
}
else if (key === '$push' || key === '$addToSet' || key === '$pull') {
if (typeof value === 'object' && value !== null) {
for (const field of Object.keys(value)) {
paths.push(field);
}
}
}
}
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
paths.push(...extractFieldPaths(value, fullKey));
}
else {
paths.push(fullKey);
}
}
return paths;
};
// Helper function to filter protected fields from query
const filterProtectedFields = (obj) => {
const filtered = JSON.parse(JSON.stringify(obj)); // Deep clone
const removed = [];
const filterRecursive = (current, path = '') => {
for (const [key, value] of Object.entries(current)) {
const fullPath = path ? `${path}.${key}` : key;
if (key.startsWith('$')) {
// Handle MongoDB operators
if (typeof value === 'object' && value !== null) {
const subResult = filterRecursive(value, fullPath);
removed.push(...subResult);
}
}
else {
// Check if this field or any parent field is protected
const isProtected = protectedFields.some(pf => key === pf || key.startsWith(pf + '.') ||
fullPath.includes('.' + pf) || fullPath.startsWith(pf + '.'));
if (isProtected) {
removed.push(key);
delete current[key];
}
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const subResult = filterRecursive(value, fullPath);
removed.push(...subResult);
}
}
}
return removed;
};
filterRecursive(filtered);
return { filtered, removed };
};
// Helper function to handle ObjectId conversions in query
const convertObjectIds = (obj) => {
if (Array.isArray(obj)) {
return obj.map(item => convertObjectIds(item));
}
else if (obj !== null && typeof obj === 'object') {
const converted = {};
for (const [key, value] of Object.entries(obj)) {
if (key === 'referenceId' && typeof value === 'string' && ObjectId.isValid(value)) {
converted[key] = new ObjectId(value);
}
else {
converted[key] = convertObjectIds(value);
}
}
return converted;
}
return obj;
};
// Helper function to calculate next pagenum for push operations
const calculateNextPageNum = (existingCasefile) => {
const maxPageNum = Math.max(...(existingCasefile.index || []).map((item) => item.pagenum || 0), ...(existingCasefile.pages || []).map((item) => item.pagenum || 0), -1);
return maxPageNum + 1;
};
// Helper function to enhance push operations with pagenum
const enhancePushOperations = (query, nextPageNum) => {
if (query.$push) {
for (const [field, value] of Object.entries(query.$push)) {
if ((field === 'pages' || field === 'index') && typeof value === 'object' && value !== null) {
if (!('pagenum' in value)) {
value.pagenum = nextPageNum;
}
}
}
}
return query;
};
// Filter protected fields
const { filtered: filteredQuery, removed: removedFields } = filterProtectedFields(mongodb_update);
if (Object.keys(filteredQuery).length === 0) {
return [{
type: "text",
text: `No valid operations provided. All operations targeted protected fields: ${removedFields.join(', ')}`
}];
}
// Convert ObjectIds
const queryWithObjectIds = convertObjectIds(filteredQuery);
// Calculate next pagenum if needed
const nextPageNum = calculateNextPageNum(existingCasefile);
const enhancedQuery = enhancePushOperations(queryWithObjectIds, nextPageNum);
// Auto-inject updatedAt timestamp
if (!enhancedQuery.$set) {
enhancedQuery.$set = {};
}
enhancedQuery.$set.updatedAt = new Date();
// Enhanced validation for flag, content, and summary dependency
const validateFlagContentSummaryDependency = (query) => {
const checkFields = (obj, path = '') => {
let hasFlag = false, hasContent = false, hasSummary = false;
for (const [key, value] of Object.entries(obj)) {
const currentPath = path ? `${path}.${key}` : key;
if (currentPath.includes('flag') || key === 'flag')
hasFlag = true;
if (currentPath.includes('content') || key === 'content')
hasContent = true;
if (currentPath.includes('summary') || key === 'summary')
hasSummary = true;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nested = checkFields(value, currentPath);
hasFlag = hasFlag || nested.flag;
hasContent = hasContent || nested.content;
hasSummary = hasSummary || nested.summary;
}
}
return { flag: hasFlag, content: hasContent, summary: hasSummary };
};
const fields = checkFields(query);
const fieldsPresent = [fields.flag, fields.content, fields.summary];
const presentCount = fieldsPresent.filter(Boolean).length;
// If any of these fields are being updated, all three must be updated together
if (presentCount > 0 && presentCount < 3) {
const missing = [];
if (!fields.flag)
missing.push('flag');
if (!fields.content)
missing.push('content');
if (!fields.summary)
missing.push('summary');
return `Error: When updating flag, content, or summary fields, all three must be updated together. Missing: ${missing.join(', ')}`;
}
return null;
};
// Check flag, content, summary dependency
const dependencyError = validateFlagContentSummaryDependency(enhancedQuery);
if (dependencyError) {
return [{
type: "text",
text: dependencyError
}];
}
// Enhanced validation for index/pages consistency
const hasIndexPush = enhancedQuery.$push && 'index' in enhancedQuery.$push;
const hasPagesPush = enhancedQuery.$push && 'pages' in enhancedQuery.$push;
const hasIndexPull = enhancedQuery.$pull && 'index' in enhancedQuery.$pull;
const hasPagesPull = enhancedQuery.$pull && 'pages' in enhancedQuery.$pull;
// Validate push operations
if (hasIndexPush && !hasPagesPush) {
return [{
type: "text",
text: "Error: Cannot push to 'index' without corresponding 'pages' push operation for data consistency."
}];
}
if (hasPagesPush && !hasIndexPush) {
return [{
type: "text",
text: "Error: Cannot push to 'pages' without corresponding 'index' push operation for data consistency."
}];
}
// Validate referenceId matching for push operations
if (hasIndexPush && hasPagesPush) {
const indexRef = enhancedQuery.$push.index?.referenceId;
const pageRef = enhancedQuery.$push.pages?.referenceId;
if (indexRef && pageRef) {
const indexRefStr = indexRef.toString();
const pageRefStr = pageRef.toString();
if (indexRefStr !== pageRefStr) {
return [{
type: "text",
text: `Error: Index and pages referenceId must match. Found index: ${indexRefStr}, pages: ${pageRefStr}`
}];
}
}
}
// Validate pull operations
if (hasIndexPull && !hasPagesPull) {
return [{
type: "text",
text: "Error: Cannot pull from 'index' without corresponding 'pages' pull operation for data consistency."
}];
}
if (hasPagesPull && !hasIndexPull) {
return [{
type: "text",
text: "Error: Cannot pull from 'pages' without corresponding 'index' pull operation for data consistency."
}];
}
// Validate pull criteria consistency
if (hasIndexPull && hasPagesPull) {
const indexPull = enhancedQuery.$pull.index;
const pagesPull = enhancedQuery.$pull.pages;
// If pulling by pagenum, ensure both have same pagenum
if (indexPull?.pagenum !== undefined && pagesPull?.pagenum !== undefined) {
if (indexPull.pagenum !== pagesPull.pagenum) {
return [{
type: "text",
text: `Error: Index and pages pull operations must target same pagenum. Found index: ${indexPull.pagenum}, pages: ${pagesPull.pagenum}`
}];
}
}
// If pulling by referenceId, ensure both have same referenceId
if (indexPull?.referenceId && pagesPull?.referenceId) {
const indexRefStr = indexPull.referenceId.toString();
const pageRefStr = pagesPull.referenceId.toString();
if (indexRefStr !== pageRefStr) {
return [{
type: "text",
text: `Error: Index and pages pull operations must target same referenceId. Found index: ${indexRefStr}, pages: ${pageRefStr}`
}];
}
}
}
// Execute the update with appropriate options
const updateOptions = { upsert: false };
if (array_filters && array_filters.length > 0) {
updateOptions.arrayFilters = array_filters;
}
const result = await collection.updateOne({ _id: new ObjectId(casefile_id) }, enhancedQuery, updateOptions);
if (result.matchedCount === 0) {
return [{
type: "text",
text: `No casefile found with ID: ${casefile_id}`
}];
}
if (result.modifiedCount === 0) {
return [{
type: "text",
text: `Casefile ${casefile_id} found but no changes were made (values may be identical)`
}];
}
// Fetch the updated document
const updatedCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
if (!updatedCasefile) {
throw new Error(`Failed to retrieve updated casefile ${casefile_id}`);
}
// Generate new link with updated casefile data
let newLink = updatedCasefile.link;
try {
newLink = await generateCasefileWeblink(casefile_id);
// Update the link in the database
await collection.updateOne({ _id: new ObjectId(casefile_id) }, { $set: { link: newLink } });
console.log(`[update_casefile] Generated new link for casefile ${casefile_id}: ${newLink}`);
}
catch (error) {
console.error(`[update_casefile] Failed to generate new link for casefile ${casefile_id}: ${error}`);
// Continue with existing link - don't fail the entire operation
}
// Fetch the final updated document with new link for sync
const finalCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
if (finalCasefile) {
try {
await pushToTypesense(finalCasefile, 'upsert');
console.log(`[update_casefile] Successfully synced casefile ${casefile_id}`);
}
catch (error) {
console.error(`[update_casefile] Sync error for casefile ${casefile_id}: ${error}`);
}
}
const response = {
casefile_id: casefile_id,
operations_executed: Object.keys(enhancedQuery),
modified_count: result.modifiedCount,
removed_protected_fields: removedFields.length > 0 ? removedFields : undefined,
auto_injected: ["updatedAt", "link"],
updated_at: enhancedQuery.$set.updatedAt.toISOString(),
new_link: newLink
};
return [{
type: "text",
title: `Casefile Updated via MongoDB Query: ${casefile_id}`,
text: JSON.stringify(response, null, 2),
format: "json"
}];
}
catch (error) {
console.error(`[update_casefile] Error: ${error}`);
return [{
type: "text",
title: "MongoDB Casefile Update Error",
text: `Failed to update casefile: ${error instanceof Error ? error.message : String(error)}`,
format: "text"
}];
}
}
},
];
// Helper function to convert casefile dates
function convertCasefileDates(document) {
const dateFields = [
'casefileInitiationDate',
'lastCasefileUpdateDate'
];
for (const field of dateFields) {
if (document[field]) {
try {
document[field] = new Date(document[field] * 1000).toISOString().slice(0, 19).replace('T', ' ');
}
catch (error) {
console.warn(`Failed to convert ${field}: ${error}`);
}
}
}
return document;
}
// Helper function to generate alpha-numeric ID
function generateAlphaNumericId(alphaSize = 3, numericSize = 3) {
const letters = 'abcdefghijklmnopqrstuvwxyz';
const digits = '0123456789';
let alphaPart = '';
let numericPart = '';
for (let i = 0; i < alphaSize; i++) {
alphaPart += letters.charAt(Math.floor(Math.random() * letters.length));
}
for (let i = 0; i < numericSize; i++) {
numericPart += digits.charAt(Math.floor(Math.random() * digits.length));
}
return alphaPart + numericPart;
}
// Helper function to generate artifacts
function getListOfArtifacts(functionName, results) {
const artifacts = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (!result.url)
continue;
const artifactData = {
id: `msg_browser_${generateAlphaNumericId()}`,
parentTaskId: `task_casefile_${generateAlphaNumericId()}`,
timestamp: Date.now(),
agent: {
id: "agent_siya_browser",
name: "SIYA",
type: "qna"
},
messageType: "action",
action: {
tool: "browser",
operation: "browsing",
params: {
url: result.title,
pageTitle: `Tool response for ${functionName}`,
visual: {
icon: "browser",
color: "#2D8CFF"
},
stream: {
type: "vnc",
streamId: "stream_browser_1",
target: "browser"
}
}
},
content: `Viewed page: ${functionName}`,
artifacts: [{
id: `artifact_webpage_${generateAlphaNumericId()}`,
type: "browser_view",
content: {
url: result.url,
title: functionName,
screenshot: "",
textContent: `Observed output of cmd \`${functionName}\` executed:`,
extractedInfo: {}
},
metadata: {
domainName: "example.com",
visitTimestamp: Date.now(),
category: "web_page"
}
}],
status: "completed"
};
artifacts.push({
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: `Casefile: ${result.title}`,
format: "json"
});
}
return artifacts;
}
// Helper to generate casefile weblink (calls diary API)
async function generateCasefileWeblink(casefileId) {
const endpoints = [
`${config.API_BASE_URL}/v1.0/diary/casefile-html/${casefileId}`,
`${config.API_BASE_URL}/v1.0/diary/casefilehtml/${casefileId}`
];
const headers = { Authorization: `Bearer ${config.API_TOKEN}` };
for (const url of endpoints) {
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200) {
const body = resp.data;
const data = body.resultData || {};
if (body.status === "OK" && data.url) {
return data.url;
}
}
}
catch (e) {
// Continue to next endpoint if error
continue;
}
}
throw new Error(`Could not generate weblink for casefile ${casefileId}`);
}
//# sourceMappingURL=index.js.map