Files
classroomio/apps/api/src/services/agent/chat-history.ts
T
Rotimi Best d5b07d356e feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
2026-06-07 18:43:05 +01:00

161 lines
3.7 KiB
TypeScript

import {
listChatConversations,
getChatConversation,
createChatConversation,
saveChatMessages as saveDbChatMessages,
deleteChatConversation,
updateConversationTitle,
getChatHistory,
saveChatHistory as saveDbChatHistory,
deleteChatHistory
} from '@cio/db/queries/agent';
const MAX_PERSISTED_TOOL_OUTPUT_CHARS = 2000;
const DROPPED_PART_TYPES = new Set(['step-start', 'reasoning']);
const TOOLS_WITH_FULL_OUTPUT = new Set([
'generate_course_plan',
'ask_template_questions',
'fetch_documentation_url',
'get_course_structure',
'get_exercise_details'
]);
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function serializeValue(value: unknown): string {
if (typeof value === 'string') {
return value;
}
try {
return JSON.stringify(value);
} catch {
return '';
}
}
function getToolName(part: Record<string, unknown>): string | null {
const type = part.type;
if (type === 'tool-invocation') {
const toolInvocation = part.toolInvocation;
return isRecord(toolInvocation) && typeof toolInvocation.toolName === 'string' ? toolInvocation.toolName : null;
}
if (typeof type === 'string' && type.startsWith('tool-')) {
return type.slice(5);
}
return null;
}
function buildTruncatedOutput(output: unknown): unknown {
const serialized = serializeValue(output);
if (serialized.length <= MAX_PERSISTED_TOOL_OUTPUT_CHARS) {
return output;
}
const preview = serialized.slice(0, MAX_PERSISTED_TOOL_OUTPUT_CHARS) + '...[truncated]';
if (typeof output === 'string') {
return preview;
}
if (!isRecord(output)) {
return preview;
}
const preservedScalars = Object.fromEntries(
Object.entries(output).filter(
([, value]) => value == null || ['string', 'number', 'boolean'].includes(typeof value)
)
);
return {
...preservedScalars,
truncated: true,
preview
};
}
function sanitizeMessagePart(part: unknown): unknown | null {
if (!isRecord(part)) {
return part;
}
const type = typeof part.type === 'string' ? part.type : '';
if (DROPPED_PART_TYPES.has(type)) {
return null;
}
const toolName = getToolName(part);
if (!toolName || TOOLS_WITH_FULL_OUTPUT.has(toolName)) {
return part;
}
if (part.type === 'tool-invocation' && isRecord(part.toolInvocation)) {
return {
...part,
toolInvocation: {
...part.toolInvocation,
result: buildTruncatedOutput(part.toolInvocation.result)
}
};
}
if (part.state !== 'output-available' || part.output == null) {
return part;
}
return {
...part,
output: buildTruncatedOutput(part.output)
};
}
function sanitizeChatMessages(messages: unknown[]): unknown[] {
return messages.map((message) => {
if (!isRecord(message) || !Array.isArray(message.parts)) {
return message;
}
const sanitizedParts = message.parts.map(sanitizeMessagePart).filter((part) => part !== null);
return {
...message,
parts: sanitizedParts
};
});
}
export async function saveChatMessages(
conversationId: string,
userId: string,
messages: unknown[],
title?: string
): Promise<void> {
await saveDbChatMessages(conversationId, userId, sanitizeChatMessages(messages), title);
}
async function persistChatHistory(courseId: string, userId: string, messages: unknown[]): Promise<void> {
await saveDbChatHistory(courseId, userId, sanitizeChatMessages(messages));
}
export {
listChatConversations,
getChatConversation,
createChatConversation,
deleteChatConversation,
updateConversationTitle,
// Legacy compat
getChatHistory as fetchChatHistory,
persistChatHistory,
deleteChatHistory as clearChatHistory
};