mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
605 lines
26 KiB
TypeScript
605 lines
26 KiB
TypeScript
/**
|
|
* Copyright 2026 Reto Meier
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { TranscriptManagerConfig,
|
|
FormattedTranscriptEntry,
|
|
FormattedTranscriptPart, USER_ROLE } from "../momoa_core/types.js";
|
|
|
|
/**
|
|
* Interface for a single entry in the transcript.
|
|
*/
|
|
interface TranscriptEntry {
|
|
speaker: string;
|
|
content: string | any[];
|
|
originalContent: string | any[];
|
|
documentId?: string;
|
|
replacementIfSuperseded?: string;
|
|
isSuperseded: boolean;
|
|
ephemeral?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Interface for the options parameter in addEntry method.
|
|
*/
|
|
export interface AddEntryOptions {
|
|
documentId?: string;
|
|
replacementIfSuperseded?: string;
|
|
ephemeral?: boolean;
|
|
}
|
|
|
|
/**
|
|
* @class TranscriptManager
|
|
* @description Manages a transcript of a conversation, with granular handling for versioned documents.
|
|
* Each document version can specify its own replacement text to be used if it's superseded.
|
|
*/
|
|
export class TranscriptManager {
|
|
private transcript: TranscriptEntry[] = [];
|
|
private readonly config: TranscriptManagerConfig;
|
|
|
|
constructor(
|
|
config: TranscriptManagerConfig,
|
|
) {
|
|
this.config = config;
|
|
}
|
|
|
|
/**
|
|
* Initializes the transcript with the initial user prompt and optional image.
|
|
* This should only be called once during construction.
|
|
* @param imagePrompt The initial text prompt from the user.
|
|
* @param image Optional Base64 encoded image data.
|
|
* @param imageMimeType Optional MIME type of the attached image.
|
|
*/
|
|
addImage(imagePrompt?: string, image?: string, imageMimeType?: string): void {
|
|
const parts: FormattedTranscriptPart[] = [];
|
|
|
|
// 1. Add Image First (Context)
|
|
// Providing the image before the text prompt is standard practice for multimodal models.
|
|
if (image && imageMimeType) {
|
|
parts.push({
|
|
inlineData: {
|
|
mimeType: imageMimeType,
|
|
data: image,
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2. Add Text Prompt Second (Question/Instruction)
|
|
if (imagePrompt) {
|
|
parts.push({ text: imagePrompt });
|
|
} else if (parts.length === 0) {
|
|
// Fallback: Ensure we have at least one part if both are missing
|
|
parts.push({ text: '' });
|
|
}
|
|
|
|
// Add the initial user entry. We use 'user' role defined in types.ts.
|
|
// We use the internal addEntry method to ensure proper TranscriptEntry creation.
|
|
// The initial entry is not ephemeral and does not require special options.
|
|
this.addEntry(USER_ROLE, parts, {}, false);
|
|
}
|
|
|
|
/**
|
|
* Adds a new entry to the transcript.
|
|
*
|
|
* If this entry represents a new version of a document (indicated by `options.documentId`),
|
|
* it will mark all previously added, non-superseded versions of the same document as superseded.
|
|
* When an older version is marked as superseded:
|
|
* - Its `isSuperseded` flag is set to true.
|
|
* - If that older version had a `replacementIfSuperseded` text defined at the time it was added,
|
|
* its `content` field will be updated to this specific replacement text.
|
|
* - If it did not have such a text defined, its `content` (which is its original content) remains unchanged,
|
|
* but it's still marked as `isSuperseded`.
|
|
*
|
|
* The new entry itself is added with its own `content`, `documentId` (if any),
|
|
* and its own `replacementIfSuperseded` text (if any, for future use).
|
|
*
|
|
* @param {string} speaker - The identifier of the speaker or entity providing the content. This will be used as 'role'.
|
|
* @param {string} content - The textual content of what was said, or the current version of a document. This will be the element in 'parts'.
|
|
* @param {AddEntryOptions} [options={}] - Optional parameters for the entry.
|
|
* @param {string} [options.documentId] - A unique identifier for the document. If provided,
|
|
* this entry is treated as a version of this document.
|
|
* @param {string} [options.replacementIfSuperseded] - The specific string that THIS entry's `content`
|
|
* should be changed to if a newer version of this same `documentId` is added in the future.
|
|
* @param {boolean} [flipLast=false] - If true, inserts the new entry before any pending user messages.
|
|
*/
|
|
addEntry(
|
|
speaker: string,
|
|
content: string | any[],
|
|
options: AddEntryOptions = {},
|
|
flipLast: boolean = false,
|
|
): void {
|
|
if (typeof speaker !== 'string' || speaker.trim() === '') {
|
|
console.warn('Warning: Speaker identifier is empty or not a string.');
|
|
}
|
|
if (typeof content !== 'string' && !Array.isArray(content)) {
|
|
console.warn('Warning: Content is not a string or array.');
|
|
}
|
|
|
|
const { documentId, replacementIfSuperseded, ephemeral } = options;
|
|
|
|
// IDEMPOTENCY CHECK:
|
|
// If an entry with this documentId already exists, acts as the same role,
|
|
// has the EXACT same content, and is currently active (not superseded),
|
|
// we ignore this addition. This prevents "echos" from external syncs.
|
|
if (documentId && typeof documentId === 'string' && documentId.trim() !== '') {
|
|
const exactDuplicate = this.transcript.find(entry =>
|
|
entry.documentId === documentId &&
|
|
!entry.isSuperseded &&
|
|
entry.speaker === speaker &&
|
|
// Use stringify to compare both strings and arrays (tool parts)
|
|
JSON.stringify(entry.content) === JSON.stringify(content)
|
|
);
|
|
|
|
if (exactDuplicate) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const newEntry: TranscriptEntry = {
|
|
speaker: speaker,
|
|
content: content,
|
|
originalContent: content,
|
|
isSuperseded: false,
|
|
...(ephemeral !== undefined && { ephemeral }),
|
|
};
|
|
|
|
if (documentId && typeof documentId === 'string' && documentId.trim() !== '') {
|
|
newEntry.documentId = documentId;
|
|
|
|
// Store the replacement rule for this specific version, if provided
|
|
if (typeof replacementIfSuperseded === 'string') {
|
|
newEntry.replacementIfSuperseded = replacementIfSuperseded;
|
|
}
|
|
|
|
// Now, iterate through existing entries to supersede older versions of THIS document.
|
|
this.transcript.forEach(entry => {
|
|
// This code will supercede entries of the form "FILENAME:LINT".
|
|
const toolReplace = (entry.documentId && entry.documentId.includes(":") && entry.documentId.split(":", 1)[0] == newEntry.documentId && !entry.isSuperseded);
|
|
|
|
if (toolReplace || (entry.documentId === newEntry.documentId && !entry.isSuperseded)) {
|
|
entry.isSuperseded = true;
|
|
if (typeof entry.replacementIfSuperseded === 'string') {
|
|
entry.content = entry.replacementIfSuperseded;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (flipLast) {
|
|
// Define the roles that identify a model response and a pending user message.
|
|
// These could be made configurable (e.g., via class properties or method parameters)
|
|
// to align with getPendingPrompt if it uses dynamic roles.
|
|
const modelRoleForPendingCheck = "model";
|
|
const userRoleForPendingCheck = "user";
|
|
|
|
let pendingBlockStartIndex = -1;
|
|
|
|
if (this.transcript.length > 0) {
|
|
// A pending prompt can only exist if the last entry is NOT a model response.
|
|
if (this.transcript[this.transcript.length - 1].speaker !== modelRoleForPendingCheck) {
|
|
let countOfPendingEntries = 0;
|
|
// Iterate backwards to find the start of the contiguous block of user entries.
|
|
for (let i = this.transcript.length - 1; i >= 0; i--) {
|
|
if (this.transcript[i].speaker === userRoleForPendingCheck) {
|
|
countOfPendingEntries++;
|
|
} else {
|
|
// The block of pending user entries ends here.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (countOfPendingEntries > 0) {
|
|
pendingBlockStartIndex = this.transcript.length - countOfPendingEntries;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pendingBlockStartIndex !== -1) {
|
|
// Insert the new entry before the identified block of pending entries.
|
|
this.transcript.splice(pendingBlockStartIndex, 0, newEntry);
|
|
} else {
|
|
// No pending entries were found (or transcript was empty).
|
|
// With `flipLast` now meaning "insert before pending", if that's not possible,
|
|
// the entry is appended. This replaces the old "swap with last" behavior.
|
|
this.transcript.push(newEntry);
|
|
}
|
|
} else { // flipLast is false
|
|
this.transcript.push(newEntry);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @method insertLineAtTop
|
|
* @description Inserts a new line of text at the beginning of the content of a transcript entry identified by its documentId.
|
|
* It iterates through the transcript and prepends the text to the content of any non-superseded entry
|
|
* found that matches the provided documentId.
|
|
*
|
|
* @param {string} identifier - The documentId of the entry to modify. This must be a non-empty string.
|
|
* @param {string} lineToInsert - The new line of text to prepend to the 'content' property.
|
|
*/
|
|
insertLineAtTop(identifier: string, lineToInsert: string): void {
|
|
// Ensure identifier is a valid non-empty string before proceeding
|
|
if (typeof identifier !== 'string' || identifier.trim() === '') {
|
|
console.warn('insertLineAtTop called with invalid or empty identifier:', identifier);
|
|
return;
|
|
}
|
|
if (typeof lineToInsert !== 'string') {
|
|
console.warn('insertLineAtTop called with non-string lineToInsert:', lineToInsert);
|
|
return;
|
|
}
|
|
|
|
// Iterate through the transcript entries to find the matching documentId
|
|
this.transcript.forEach(entry => {
|
|
// Check if the entry has a documentId, it matches the provided identifier, and it's not superseded
|
|
if (entry.documentId === identifier && !entry.isSuperseded) {
|
|
// Prepend the new line to the entry's content, only if content is string
|
|
if (typeof entry.content === 'string') {
|
|
entry.content = `${lineToInsert}\n${entry.content}`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @method replaceEntry
|
|
* @description Replaces the content of a transcript entry identified by its documentId.
|
|
* It iterates through the transcript and replaces the entries found that match the provided documentId.
|
|
* If no entry is found with the specified documentId, the method does nothing.
|
|
*
|
|
* @param {string} identifier - The documentId of the entry to replace. This must be a non-empty string.
|
|
* @param {string} newValue - The new value for the 'content' property of the matching entry.
|
|
*/
|
|
replaceEntry(identifier: string, newValue: string): void {
|
|
// Ensure identifier is a valid non-empty string before proceeding
|
|
if (!identifier) {
|
|
return; // Do nothing if identifier is invalid
|
|
}
|
|
|
|
// Iterate through the transcript entries to find the matching documentId
|
|
for (let i = 0; i < this.transcript.length; i++) {
|
|
const entry = this.transcript[i];
|
|
// Check if the entry has a documentId property and it matches the provided identifier
|
|
if (entry.documentId === identifier) {
|
|
// Found the entry, update its content with the new value
|
|
entry.content = newValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @method supersedeEntry
|
|
* @description Finds all non-superseded entries with a given documentId and marks them as superseded.
|
|
* If an entry has a 'replacementIfSuperseded' text, its content is updated to that text.
|
|
* This method does NOT add a new entry to the transcript.
|
|
*
|
|
* @param {string} identifier - The documentId of the entry/entries to supersede.
|
|
*/
|
|
supersedeEntry(identifier: string): void {
|
|
if (typeof identifier !== 'string' || identifier.trim() === '') {
|
|
console.warn('supersedeEntry called with invalid or empty identifier:', identifier);
|
|
return;
|
|
}
|
|
|
|
this.transcript.forEach(entry => {
|
|
// Check if the entry matches the documentId and is not already superseded.
|
|
if (entry.documentId === identifier && !entry.isSuperseded) {
|
|
entry.isSuperseded = true;
|
|
// If a specific replacement string was defined for this entry, use it.
|
|
if (typeof entry.replacementIfSuperseded === 'string') {
|
|
entry.content = entry.replacementIfSuperseded;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves the transcript, formatting each entry into the specified structure:
|
|
* `{'role': <speaker_identifier>, 'parts': [{'text': <content_string>}]}`.
|
|
* Consecutive entries from the same speaker are merged using a newline character as a separator.
|
|
*
|
|
* If `lastSpeakerRequired` is provided (e.g., 'Model' or 'User'), the returned transcript
|
|
* will be truncated to end with the last consolidated entry from that specified speaker.
|
|
* Any entries that originally followed this last instance of `lastSpeakerRequired` will be ignored.
|
|
* If the `lastSpeakerRequired` is specified but not found in the transcript, an empty array is returned.
|
|
*
|
|
* @param {string} [lastSpeakerRequired] - Optional. The speaker role (e.g., 'Model', 'User')
|
|
* that must be the final speaker in the returned transcript.
|
|
* If falsy (e.g., undefined, null, empty string), this filtering is not applied.
|
|
* @returns {FormattedTranscriptEntry[]} An array of formatted transcript entries.
|
|
*/
|
|
getTranscript(lastSpeakerRequired?: string, excludeEphemeral: boolean = false): FormattedTranscriptEntry[] {
|
|
// Handle cases where the transcript itself is null or undefined.
|
|
if (!this.transcript) {
|
|
return [];
|
|
}
|
|
|
|
// Step 1: Perform the consolidation of transcript entries.
|
|
// This reduce operation will return an empty array if this.transcript is empty.
|
|
const consolidatedTranscript: FormattedTranscriptEntry[] = this.transcript
|
|
.filter(entry => !excludeEphemeral || !entry.ephemeral)
|
|
//.filter(entry => !entry.isSuperseded)
|
|
.reduce((accumulator: FormattedTranscriptEntry[], currentEntry: TranscriptEntry) => {
|
|
const speaker = currentEntry.speaker;
|
|
const content = currentEntry.content;
|
|
const isCurrentEphemeral = !!currentEntry.ephemeral;
|
|
|
|
const lastEntry = accumulator.length > 0 ? accumulator[accumulator.length - 1] : null;
|
|
|
|
if (lastEntry && lastEntry.role === speaker) {
|
|
// Same role, try to merge content
|
|
|
|
if (typeof content === 'string') {
|
|
// Current content is text.
|
|
// Check if the last part of the accumulated entry is also text.
|
|
const lastPartIndex = lastEntry.parts.length - 1;
|
|
if (lastPartIndex >= 0 && 'text' in lastEntry.parts[lastPartIndex]) {
|
|
// Merge text with newline
|
|
lastEntry.parts[lastPartIndex].text += "\n" + content;
|
|
} else {
|
|
// Previous part is not text (e.g. function call), so append new text part
|
|
lastEntry.parts.push({ text: content });
|
|
}
|
|
} else if (Array.isArray(content)) {
|
|
// Do NOT map to text here. Pass the objects through.
|
|
lastEntry.parts.push(...content);
|
|
}
|
|
|
|
// If the existing entry is ephemeral, and the current entry is NOT,
|
|
// the resulting entry must be marked non-ephemeral (i.e., remove the flag).
|
|
if (lastEntry.ephemeral && !isCurrentEphemeral) {
|
|
delete lastEntry.ephemeral;
|
|
}
|
|
} else {
|
|
// Different role or the accumulator is empty, so add a new entry.
|
|
let parts: any[] = [];
|
|
|
|
if (typeof content === 'string') {
|
|
parts = [{ text: content }];
|
|
} else if (Array.isArray(content)) {
|
|
parts = [...content];
|
|
}
|
|
|
|
const newEntry: FormattedTranscriptEntry = {
|
|
role: speaker,
|
|
parts: parts
|
|
};
|
|
if (isCurrentEphemeral) {
|
|
newEntry.ephemeral = true;
|
|
}
|
|
accumulator.push(newEntry);
|
|
}
|
|
return accumulator;
|
|
}, []);
|
|
|
|
// If the consolidation results in an empty transcript (e.g., original was empty),
|
|
// no further processing based on lastSpeakerRequired is needed.
|
|
if (consolidatedTranscript.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Step 2: If lastSpeakerRequired is not provided (or is falsy),
|
|
// return the full consolidated transcript.
|
|
if (!lastSpeakerRequired) {
|
|
return consolidatedTranscript;
|
|
}
|
|
|
|
// Step 3: If lastSpeakerRequired is provided, find the last occurrence of this speaker.
|
|
let lastIndexOfRequiredSpeaker = -1;
|
|
for (let i = consolidatedTranscript.length - 1; i >= 0; i--) {
|
|
if (consolidatedTranscript[i].role === lastSpeakerRequired) {
|
|
lastIndexOfRequiredSpeaker = i;
|
|
break; // Found the last instance, no need to search further.
|
|
}
|
|
}
|
|
|
|
// Step 4: Handle based on whether the required speaker was found.
|
|
if (lastIndexOfRequiredSpeaker !== -1) {
|
|
// Truncate the array to include entries up to and including the last required speaker.
|
|
return consolidatedTranscript.slice(0, lastIndexOfRequiredSpeaker + 1);
|
|
} else {
|
|
// The required speaker was not found in the consolidated transcript.
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears all entries from the transcript.
|
|
*/
|
|
clearTranscript(): void {
|
|
this.transcript = [];
|
|
}
|
|
|
|
/**
|
|
* @method replaceLastEntryContent
|
|
* @description Replaces the content of the very last entry in the transcript.
|
|
* If the transcript is empty, this method does nothing.
|
|
* This also updates the 'originalContent' of that entry to the new content.
|
|
*
|
|
* @param {string} newContent - The new value for the 'content' and 'originalContent' properties.
|
|
*/
|
|
replaceLastEntryContent(newContent: string): void {
|
|
if (this.transcript.length === 0) {
|
|
console.warn('replaceLastEntryContent called on an empty transcript.');
|
|
return;
|
|
}
|
|
|
|
if (typeof newContent !== 'string') {
|
|
console.warn('replaceLastEntryContent called with non-string newContent.');
|
|
return;
|
|
}
|
|
|
|
const lastEntry = this.transcript[this.transcript.length - 1];
|
|
lastEntry.content = newContent;
|
|
lastEntry.originalContent = newContent; // Also update originalContent to match
|
|
}
|
|
|
|
/**
|
|
* Retrieves the concatenated content of 'User' entries that form a pending prompt
|
|
* at the end of the transcript.
|
|
*
|
|
* The logic is as follows:
|
|
* 1. If the transcript is empty or the last entry is from 'Model',
|
|
* it means there's no pending 'User' prompt, so it returns `null`.
|
|
* 2. Otherwise (if the last entry is from 'User'), it iterates backward from the end
|
|
* of the transcript, collecting the 'content' of all contiguous 'User' entries.
|
|
* 3. These collected 'content' strings are then concatenated (with spaces) in their
|
|
* original order and returned.
|
|
*
|
|
* This method helps identify user inputs that are awaiting a response from the model.
|
|
*
|
|
* @returns {string | null} The concatenated string of pending 'User' messages,
|
|
* or `null` if no such prompt is found or if the Model spoke last.
|
|
*/
|
|
getPendingPrompt(): string | null {
|
|
if (!this.transcript || this.transcript.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const lastEntry = this.transcript[this.transcript.length - 1];
|
|
|
|
// If the last entry in the transcript was from the 'Model',
|
|
// then there is no pending prompt from the 'User'.
|
|
if (lastEntry.speaker === "model") {
|
|
return null;
|
|
}
|
|
|
|
// If we're here, the last entry was not 'Model'.
|
|
// We now collect all contiguous 'User' entries from the end.
|
|
const pendingUserContents: string[] = [];
|
|
for (let i = this.transcript.length - 1; i >= 0; i--) {
|
|
const entry = this.transcript[i];
|
|
if (entry.speaker === "user") {
|
|
// Add to the beginning of the array to maintain the original order of messages
|
|
if (typeof entry.content === 'string') {
|
|
pendingUserContents.unshift(entry.content);
|
|
}
|
|
} else {
|
|
// We encountered a non-User entry (e.g., 'Model'),
|
|
// so the sequence of pending User messages has ended.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If, after checking, no User content was found (e.g., transcript ended with a non-Model, non-User entry,
|
|
// or User entries had empty content - though this method assumes content is present), return null.
|
|
if (pendingUserContents.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Join the collected User contents with a space.
|
|
return pendingUserContents.join("\n");
|
|
}
|
|
|
|
getFullTranscriptAsString() {
|
|
if (!this.transcript || this.transcript.length == 0)
|
|
return 'NO TRANSCRIPT';
|
|
|
|
let chatHistoryArray = [...this.transcript];
|
|
|
|
// Map over the array to format each message object into a string
|
|
const formattedMessages = chatHistoryArray.map((message, _index) => {
|
|
const role = message.speaker; // 'user' or 'model'
|
|
let text = message.content;
|
|
|
|
if (Array.isArray(text)) {
|
|
text = JSON.stringify(text);
|
|
}
|
|
return `${role}: ${text}`;
|
|
});
|
|
|
|
const historyString = formattedMessages.join("\n");
|
|
return historyString;
|
|
}
|
|
|
|
getTranscriptAsString(trimPreamble: boolean, expertNames: string[]): string {
|
|
if (!this.transcript || this.transcript.length == 0)
|
|
return '';
|
|
|
|
let chatHistoryArray = [...this.transcript];
|
|
|
|
// Only slice if we are actually trimming the preamble
|
|
if (trimPreamble && chatHistoryArray.length > 0)
|
|
chatHistoryArray = chatHistoryArray.slice(1);
|
|
|
|
// Map over the array to format each message object into a string
|
|
const formattedMessages = chatHistoryArray.map((message, index) => {
|
|
let text = message.content;
|
|
|
|
// Check if content is an array (which holds the structured parts)
|
|
if (Array.isArray(text)) {
|
|
// Map the parts to a string, handling images specifically
|
|
text = text.map(part => {
|
|
if (part.text) {
|
|
return part.text;
|
|
} else if (part.inlineData) {
|
|
// REPLACEMENT: Return a placeholder instead of the binary data
|
|
return `[Image Attachment: ${part.inlineData.mimeType}]`;
|
|
}
|
|
return '';
|
|
}).join('\n');
|
|
}
|
|
|
|
// Capitalize role for readability
|
|
let offset = 0;
|
|
let formattedRole;
|
|
|
|
if (!trimPreamble && index == 0) {
|
|
formattedRole = "User";
|
|
offset = 1;
|
|
} else {
|
|
// Existing round-robin logic for expert names
|
|
formattedRole = expertNames[(index+offset) % expertNames.length];
|
|
}
|
|
|
|
return `${formattedRole}: ${text}`;
|
|
});
|
|
|
|
const historyString = formattedMessages.join("\n");
|
|
return historyString;
|
|
}
|
|
|
|
async cleanLLMResponse(response: string): Promise<string> {
|
|
const stopStringsArray = (await this.config.context.getAssetString('response-stop-strings'))
|
|
.split('\n')
|
|
.filter(s => s.trim() !== '');
|
|
const allToolInvocationStrings = [...this.config.context.getToolNames(), `STARTWORKPHASE`, `RETURN`, 'TOOL_CALL:FINISH'];
|
|
|
|
const toolPrefix = await this.config.context.getAssetString('tool-prefix');
|
|
const lines = response.split('\n');
|
|
const cleanedLines: string[] = [];
|
|
let toolInvocationCount = 0;
|
|
|
|
for (const line of lines) {
|
|
// Rule 1: Truncate if a stop string is found on a line by itself.
|
|
if (stopStringsArray.includes(line.trim())) {
|
|
break;
|
|
}
|
|
|
|
// Rule 2: Truncate before the second tool invocation.
|
|
const isToolInvocation = allToolInvocationStrings.some((toolString: string) => line.startsWith(`${toolPrefix}${toolString}`));
|
|
if (isToolInvocation) {
|
|
toolInvocationCount++;
|
|
if (toolInvocationCount >= 2)
|
|
break;
|
|
}
|
|
|
|
cleanedLines.push(line);
|
|
}
|
|
|
|
return cleanedLines.join('\n').trimEnd();
|
|
}
|
|
} |