mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
chore(eslint): enable no-explicit-any; fix ~395 violations (#2452)
* chore(eslint): enable no-explicit-any; fix ~395 violations Switches @typescript-eslint/no-explicit-any from "off" to "error" and replaces explicit `any` with proper types or `unknown` + narrowing across ~100 source files and 15 test files. Eleven eslint-disable comments remain: one for a BaseLanguageModel prototype patch and ten for Orama<any> API surfaces where typed alternatives poison Orama's internal inference to `never`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scripts): cast chainContext to ChainManager in printPromptDebugEntry The earlier `as any` → `as unknown` rewrite left buildAgentPromptDebugReport's chainManager argument typed as `unknown`, which CI's `tsc -noEmit` (without `--skipLibCheck`) flagged. Restore the cast through the real ChainManager type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(eslint): fix remaining no-explicit-any and unnecessary-type-assertion errors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(eslint): drop redundant no-explicit-any rule Already set to "error" by typescript-eslint/recommendedTypeChecked via obsidianmd's recommended config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e1385d95f1
commit
6ca2dc01ea
107 changed files with 1322 additions and 853 deletions
|
|
@ -226,7 +226,6 @@ export default [
|
|||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type ChainManager from "@/LLMProviders/chainManager";
|
||||
import MemoryManager from "@/LLMProviders/memoryManager";
|
||||
import { ModelAdapterFactory } from "@/LLMProviders/chainRunner/utils/modelAdapter";
|
||||
import { buildAgentPromptDebugReport } from "@/LLMProviders/chainRunner/utils/promptDebugService";
|
||||
|
|
@ -116,7 +117,7 @@ export async function run(args: string[]): Promise<void> {
|
|||
const chainContext = {
|
||||
memoryManager,
|
||||
userMemoryManager,
|
||||
} as any;
|
||||
} as unknown as ChainManager;
|
||||
|
||||
const adapter = ModelAdapterFactory.createAdapter({
|
||||
modelName: "gpt-4",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,6 @@ export class Modal {
|
|||
}
|
||||
}
|
||||
|
||||
export function parseYaml(_: string): any {
|
||||
export function parseYaml(_: string): unknown {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ describe("BedrockChatModel streaming decode", () => {
|
|||
|
||||
// Check that content is an array with thinking type
|
||||
expect(Array.isArray(chunk?.message.content)).toBe(true);
|
||||
const content = chunk?.message.content as any[];
|
||||
const content = chunk?.message.content as unknown[];
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0]).toEqual({
|
||||
type: "thinking",
|
||||
|
|
@ -276,7 +276,7 @@ describe("BedrockChatModel streaming decode", () => {
|
|||
|
||||
// Check that content is an array with text type
|
||||
expect(Array.isArray(chunk?.message.content)).toBe(true);
|
||||
const content = chunk?.message.content as any[];
|
||||
const content = chunk?.message.content as unknown[];
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0]).toEqual({
|
||||
type: "text",
|
||||
|
|
@ -317,7 +317,7 @@ describe("BedrockChatModel streaming decode", () => {
|
|||
|
||||
expect(thinkingResult.deltaChunks).toHaveLength(1);
|
||||
const thinkingChunk = thinkingResult.deltaChunks[0];
|
||||
expect((thinkingChunk?.message.content as any[])[0]?.type).toBe("thinking");
|
||||
expect((thinkingChunk?.message.content as Array<{ type: string }>)[0]?.type).toBe("thinking");
|
||||
|
||||
// Second chunk: text
|
||||
const textPayload = JSON.stringify({
|
||||
|
|
@ -346,7 +346,7 @@ describe("BedrockChatModel streaming decode", () => {
|
|||
|
||||
expect(textResult.deltaChunks).toHaveLength(1);
|
||||
const textChunk = textResult.deltaChunks[0];
|
||||
expect((textChunk?.message.content as any[])[0]?.type).toBe("text");
|
||||
expect((textChunk?.message.content as Array<{ type: string }>)[0]?.type).toBe("text");
|
||||
});
|
||||
|
||||
it("extractStreamText can fallback to extract thinking content", () => {
|
||||
|
|
|
|||
|
|
@ -133,12 +133,18 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
/**
|
||||
* Convert LangChain tools to Claude's tool format for Bedrock.
|
||||
*/
|
||||
private convertToolsToClaude(tools: StructuredToolInterface[]): any[] {
|
||||
private convertToolsToClaude(tools: StructuredToolInterface[]): Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: Record<string, unknown>;
|
||||
}> {
|
||||
return tools.map((tool) => {
|
||||
let inputSchema: any = { type: "object", properties: {} };
|
||||
let inputSchema: Record<string, unknown> = { type: "object", properties: {} };
|
||||
if (tool.schema) {
|
||||
// Use LangChain's schema conversion utilities
|
||||
inputSchema = isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema;
|
||||
inputSchema = (
|
||||
isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
return {
|
||||
name: tool.name,
|
||||
|
|
@ -151,19 +157,26 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
/**
|
||||
* Extract tool calls from Claude's response format.
|
||||
*/
|
||||
private extractToolCalls(data: any): any[] | undefined {
|
||||
if (!Array.isArray(data?.content)) return undefined;
|
||||
private extractToolCalls(
|
||||
data: unknown
|
||||
):
|
||||
| Array<{ id: string; name: string; args: Record<string, unknown>; type: "tool_call" }>
|
||||
| undefined {
|
||||
const dataObj = data as Record<string, unknown> | null | undefined;
|
||||
if (!Array.isArray(dataObj?.content)) return undefined;
|
||||
|
||||
const toolUseBlocks: any[] = data.content.filter((block: any) => block.type === "tool_use");
|
||||
const toolUseBlocks = (dataObj.content as Record<string, unknown>[]).filter(
|
||||
(block) => block.type === "tool_use"
|
||||
);
|
||||
|
||||
if (toolUseBlocks.length === 0) return undefined;
|
||||
|
||||
return toolUseBlocks.map((block: any) => ({
|
||||
return toolUseBlocks.map((block) => ({
|
||||
id: block.id as string,
|
||||
name: block.name as string,
|
||||
args: (block.input || {}) as Record<string, unknown>,
|
||||
type: "tool_call" as const,
|
||||
})) as Array<{ id: string; name: string; args: Record<string, unknown>; type: "tool_call" }>;
|
||||
}));
|
||||
}
|
||||
|
||||
async _generate(
|
||||
|
|
@ -188,7 +201,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
throw new Error(`Amazon Bedrock request failed with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
const text = this.extractText(data);
|
||||
const toolCalls = this.extractToolCalls(data);
|
||||
|
||||
|
|
@ -306,13 +319,14 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
byteBuffer = new Uint8Array(remainingBytes);
|
||||
|
||||
for (const messagePayload of messages) {
|
||||
const outerEvent = this.safeJsonParse(messagePayload);
|
||||
if (!outerEvent) {
|
||||
const parsedOuter = this.safeJsonParse(messagePayload);
|
||||
if (!parsedOuter || typeof parsedOuter !== "object") {
|
||||
logWarn(
|
||||
`[${requestId}] Failed to parse event JSON: ${messagePayload.slice(0, 100)}...`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const outerEvent = parsedOuter as Record<string, unknown>;
|
||||
|
||||
// Handle AWS EventStream format where bytes field is at top level
|
||||
let eventToProcess = outerEvent;
|
||||
|
|
@ -404,7 +418,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
}
|
||||
|
||||
private safeJsonParse(value: string): any {
|
||||
private safeJsonParse(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
|
|
@ -420,18 +434,21 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
* @returns Claude-style content array or null if not classifiable
|
||||
*/
|
||||
private buildContentItemsFromDelta(
|
||||
event: any
|
||||
event: Record<string, unknown>
|
||||
): Array<{ type: string; text?: string; thinking?: string }> | null {
|
||||
if (!event || typeof event !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for content_block_delta format (nested delta)
|
||||
const delta = event.content_block_delta?.delta || event.contentBlockDelta?.delta || event.delta;
|
||||
const contentBlockDelta = event.content_block_delta as Record<string, unknown> | undefined;
|
||||
const contentBlockDeltaCamel = event.contentBlockDelta as Record<string, unknown> | undefined;
|
||||
const deltaUnknown = contentBlockDelta?.delta ?? contentBlockDeltaCamel?.delta ?? event.delta;
|
||||
|
||||
if (!delta || typeof delta !== "object") {
|
||||
if (!deltaUnknown || typeof deltaUnknown !== "object") {
|
||||
return null;
|
||||
}
|
||||
const delta = deltaUnknown as Record<string, unknown>;
|
||||
|
||||
const deltaType = delta.type;
|
||||
|
||||
|
|
@ -461,27 +478,29 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
* Returns tool_call_chunks format that LangChain can concatenate.
|
||||
*/
|
||||
private extractToolCallChunk(
|
||||
event: any
|
||||
event: Record<string, unknown>
|
||||
): { id?: string; index: number; name?: string; args?: string } | null {
|
||||
if (!event || typeof event !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle content_block_start with tool_use - initial tool call info
|
||||
if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
|
||||
const contentBlock = event.content_block as Record<string, unknown> | undefined;
|
||||
if (event.type === "content_block_start" && contentBlock?.type === "tool_use") {
|
||||
return {
|
||||
id: event.content_block.id,
|
||||
index: event.index ?? 0,
|
||||
name: event.content_block.name,
|
||||
id: contentBlock.id as string | undefined,
|
||||
index: typeof event.index === "number" ? event.index : 0,
|
||||
name: contentBlock.name as string | undefined,
|
||||
args: "",
|
||||
};
|
||||
}
|
||||
|
||||
// Handle content_block_delta with input_json_delta - partial tool args
|
||||
if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
|
||||
const eventDelta = event.delta as Record<string, unknown> | undefined;
|
||||
if (event.type === "content_block_delta" && eventDelta?.type === "input_json_delta") {
|
||||
return {
|
||||
index: event.index ?? 0,
|
||||
args: event.delta.partial_json || "",
|
||||
index: typeof event.index === "number" ? event.index : 0,
|
||||
args: typeof eventDelta.partial_json === "string" ? eventDelta.partial_json : "",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -489,7 +508,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
|
||||
private async processStreamEvent(
|
||||
event: any,
|
||||
event: Record<string, unknown>,
|
||||
runManager: CallbackManagerForLLMRun | undefined,
|
||||
currentUsage?: Record<string, unknown>,
|
||||
currentStopReason?: string
|
||||
|
|
@ -506,23 +525,25 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
let hasText = false;
|
||||
const debugSummaries: string[] = [];
|
||||
|
||||
if (event?.type === "chunk" && typeof event.chunk?.bytes === "string") {
|
||||
const decodedPayloads = this.decodeChunkBytes(event.chunk.bytes as string);
|
||||
const eventChunk = event.chunk as Record<string, unknown> | undefined;
|
||||
if (event?.type === "chunk" && typeof eventChunk?.bytes === "string") {
|
||||
const decodedPayloads = this.decodeChunkBytes(eventChunk.bytes);
|
||||
|
||||
for (const payload of decodedPayloads) {
|
||||
const innerEvent = this.safeJsonParse(payload);
|
||||
if (!innerEvent) {
|
||||
const parsedInner = this.safeJsonParse(payload);
|
||||
if (!parsedInner || typeof parsedInner !== "object") {
|
||||
debugSummaries.push(`Failed to parse inner payload: ${this.describePayload(payload)}`);
|
||||
continue;
|
||||
}
|
||||
const innerEvent = parsedInner as Record<string, unknown>;
|
||||
|
||||
const chunkMetadata = this.buildChunkMetadata(innerEvent as Record<string, unknown>);
|
||||
const chunkMetadata = this.buildChunkMetadata(innerEvent);
|
||||
|
||||
// Try to build structured content (Claude-style arrays)
|
||||
const contentItems = this.buildContentItemsFromDelta(innerEvent as Record<string, unknown>);
|
||||
const contentItems = this.buildContentItemsFromDelta(innerEvent);
|
||||
|
||||
// Check for tool call chunks first (content_block_start with tool_use, or input_json_delta)
|
||||
const toolCallChunk = this.extractToolCallChunk(innerEvent as Record<string, unknown>);
|
||||
const toolCallChunk = this.extractToolCallChunk(innerEvent);
|
||||
if (toolCallChunk) {
|
||||
const messageChunk = new AIMessageChunk({
|
||||
content: "",
|
||||
|
|
@ -595,9 +616,10 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
} else {
|
||||
// Only log if it's an unexpected event type that should have had content
|
||||
// But don't warn for tool-related events which are handled above
|
||||
const innerDelta = innerEvent.delta as Record<string, unknown> | undefined;
|
||||
if (
|
||||
innerEvent.type === "content_block_delta" &&
|
||||
innerEvent.delta?.type !== "input_json_delta"
|
||||
innerDelta?.type !== "input_json_delta"
|
||||
) {
|
||||
const summary = `No content in content_block_delta event: ${this.describeEvent(innerEvent)}`;
|
||||
debugSummaries.push(summary);
|
||||
|
|
@ -617,13 +639,13 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
}
|
||||
} else {
|
||||
const chunkMetadata = this.buildChunkMetadata(event as Record<string, unknown>);
|
||||
const chunkMetadata = this.buildChunkMetadata(event);
|
||||
|
||||
// Try to build structured content (Claude-style arrays)
|
||||
const contentItems = this.buildContentItemsFromDelta(event as Record<string, unknown>);
|
||||
const contentItems = this.buildContentItemsFromDelta(event);
|
||||
|
||||
// Check for tool call chunks first
|
||||
const toolCallChunk = this.extractToolCallChunk(event as Record<string, unknown>);
|
||||
const toolCallChunk = this.extractToolCallChunk(event);
|
||||
if (toolCallChunk) {
|
||||
const messageChunk = new AIMessageChunk({
|
||||
content: "",
|
||||
|
|
@ -942,18 +964,28 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
return metadata;
|
||||
}
|
||||
|
||||
private extractStreamText(event: any): string | null {
|
||||
private extractStreamText(event: Record<string, unknown>): string | null {
|
||||
if (!event || typeof event !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const evDelta = event.delta as Record<string, unknown> | undefined;
|
||||
const evContentBlockDelta = event.content_block_delta as Record<string, unknown> | undefined;
|
||||
const evContentBlockDeltaCamel = event.contentBlockDelta as Record<string, unknown> | undefined;
|
||||
const evContentBlockDeltaInner = evContentBlockDelta?.delta as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const evContentBlockDeltaCamelInner = evContentBlockDeltaCamel?.delta as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
// Check for thinking/reasoning fields first (fallback for non-streaming or debugging)
|
||||
// This ensures thinking content is never silently dropped
|
||||
const thinkingCandidates: Array<unknown> = [
|
||||
event.delta?.thinking,
|
||||
event.content_block_delta?.delta?.thinking,
|
||||
event.contentBlockDelta?.delta?.thinking,
|
||||
event.delta?.reasoning_content, // Deepseek compatibility
|
||||
evDelta?.thinking,
|
||||
evContentBlockDeltaInner?.thinking,
|
||||
evContentBlockDeltaCamelInner?.thinking,
|
||||
evDelta?.reasoning_content, // Deepseek compatibility
|
||||
event.reasoning_content,
|
||||
];
|
||||
|
||||
|
|
@ -977,19 +1009,27 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
}
|
||||
|
||||
const evMessage = event.message as Record<string, unknown> | undefined;
|
||||
const evMessageStop = event.messageStop as Record<string, unknown> | undefined;
|
||||
const evMessageStopSnake = event.message_stop as Record<string, unknown> | undefined;
|
||||
const evMessageStopMsg = evMessageStop?.message as Record<string, unknown> | undefined;
|
||||
const evMessageStopSnakeMsg = evMessageStopSnake?.message as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
const nestedCandidates: Array<unknown> = [
|
||||
event.delta?.text,
|
||||
event.delta?.output_text,
|
||||
event.delta?.content,
|
||||
event.contentBlockDelta?.delta?.text,
|
||||
event.contentBlockDelta?.delta?.output_text,
|
||||
event.contentBlockDelta?.delta?.content,
|
||||
event.content_block_delta?.delta?.text,
|
||||
event.content_block_delta?.delta?.output_text,
|
||||
event.content_block_delta?.delta?.content,
|
||||
event.message?.content,
|
||||
event.messageStop?.message?.content,
|
||||
event.message_stop?.message?.content,
|
||||
evDelta?.text,
|
||||
evDelta?.output_text,
|
||||
evDelta?.content,
|
||||
evContentBlockDeltaCamelInner?.text,
|
||||
evContentBlockDeltaCamelInner?.output_text,
|
||||
evContentBlockDeltaCamelInner?.content,
|
||||
evContentBlockDeltaInner?.text,
|
||||
evContentBlockDeltaInner?.output_text,
|
||||
evContentBlockDeltaInner?.content,
|
||||
evMessage?.content,
|
||||
evMessageStopMsg?.content,
|
||||
evMessageStopSnakeMsg?.content,
|
||||
event.content,
|
||||
];
|
||||
|
||||
|
|
@ -1014,20 +1054,24 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
|
||||
if (Array.isArray(candidate)) {
|
||||
const combined = candidate
|
||||
.map((part: any): string => {
|
||||
.map((part: unknown): string => {
|
||||
if (typeof part === "string") {
|
||||
return part;
|
||||
}
|
||||
if (part && typeof part === "object") {
|
||||
if (typeof part.text === "string") {
|
||||
return part.text as string;
|
||||
const partObj = part as Record<string, unknown>;
|
||||
if (typeof partObj.text === "string") {
|
||||
return partObj.text;
|
||||
}
|
||||
if (typeof part.value === "string") {
|
||||
return part.value as string;
|
||||
if (typeof partObj.value === "string") {
|
||||
return partObj.value;
|
||||
}
|
||||
if (Array.isArray(part.content)) {
|
||||
return (part.content as any[])
|
||||
.map((sub: any) => (typeof sub?.text === "string" ? (sub.text as string) : ""))
|
||||
if (Array.isArray(partObj.content)) {
|
||||
return (partObj.content as unknown[])
|
||||
.map((sub: unknown) => {
|
||||
const subObj = sub as Record<string, unknown> | null | undefined;
|
||||
return typeof subObj?.text === "string" ? subObj.text : "";
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
|
@ -1071,7 +1115,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
return null;
|
||||
}
|
||||
|
||||
private extractUsage(event: any): Record<string, unknown> | undefined {
|
||||
private extractUsage(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||
if (!event || typeof event !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1093,29 +1137,31 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
|
||||
if (event.messageStop && typeof event.messageStop === "object") {
|
||||
return this.extractUsage(event.messageStop);
|
||||
return this.extractUsage(event.messageStop as Record<string, unknown>);
|
||||
}
|
||||
|
||||
if (event.message_stop && typeof event.message_stop === "object") {
|
||||
return this.extractUsage(event.message_stop);
|
||||
return this.extractUsage(event.message_stop as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private extractStopReason(event: any): string | undefined {
|
||||
private extractStopReason(event: Record<string, unknown>): string | undefined {
|
||||
if (!event || typeof event !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const evMessageStop = event.messageStop as Record<string, unknown> | undefined;
|
||||
const evMessageStopSnake = event.message_stop as Record<string, unknown> | undefined;
|
||||
const stopReason =
|
||||
event.stop_reason ||
|
||||
event.stopReason ||
|
||||
event.completionReason ||
|
||||
event.completion_reason ||
|
||||
event.reason ||
|
||||
event.messageStop?.stopReason ||
|
||||
event.message_stop?.stop_reason ||
|
||||
evMessageStop?.stopReason ||
|
||||
evMessageStopSnake?.stop_reason ||
|
||||
(event.type === "message_stop" ? event.reason : undefined);
|
||||
|
||||
return typeof stopReason === "string" ? stopReason : undefined;
|
||||
|
|
@ -1342,9 +1388,13 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
type: "text",
|
||||
text: block.text,
|
||||
});
|
||||
} else if (block.type === "image_url" && block.image_url?.url) {
|
||||
} else if (
|
||||
block.type === "image_url" &&
|
||||
(block.image_url as Record<string, unknown> | undefined)?.url
|
||||
) {
|
||||
// Image block in OpenAI format - convert to Claude format
|
||||
const claudeImage = this.convertImageContent(block.image_url.url as string);
|
||||
const imageUrlBlock = block.image_url as Record<string, unknown>;
|
||||
const claudeImage = this.convertImageContent(imageUrlBlock.url as string);
|
||||
if (claudeImage) {
|
||||
contentBlocks.push(claudeImage);
|
||||
}
|
||||
|
|
@ -1423,7 +1473,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
*/
|
||||
private normaliseMessageContent(
|
||||
message: BaseMessage
|
||||
): string | Array<{ type: string; [key: string]: any }> {
|
||||
): string | Array<{ type: string; [key: string]: unknown }> {
|
||||
const { content } = message;
|
||||
|
||||
// Handle string content (simple text message)
|
||||
|
|
@ -1463,7 +1513,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
return null;
|
||||
})
|
||||
.filter((part): part is { type: string; [key: string]: any } => part !== null);
|
||||
.filter((part): part is { type: string; [key: string]: unknown } => part !== null);
|
||||
}
|
||||
|
||||
// No images, flatten to string
|
||||
|
|
@ -1494,20 +1544,21 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
return "";
|
||||
}
|
||||
|
||||
private extractText(data: any): string {
|
||||
private extractText(data: Record<string, unknown>): string {
|
||||
if (typeof data?.outputText === "string") {
|
||||
return data.outputText as string;
|
||||
return data.outputText;
|
||||
}
|
||||
|
||||
if (Array.isArray(data?.content)) {
|
||||
return (data.content as any[])
|
||||
.map((item: any): string => {
|
||||
return (data.content as unknown[])
|
||||
.map((item: unknown): string => {
|
||||
if (!item) return "";
|
||||
if (typeof item === "string") return item;
|
||||
if (typeof item === "object") {
|
||||
if (typeof item.text === "string") return item.text as string;
|
||||
if (item.text && typeof item.text === "object" && "text" in item.text) {
|
||||
return (item.text.text as string | undefined) ?? "";
|
||||
const itemObj = item as Record<string, unknown>;
|
||||
if (typeof itemObj.text === "string") return itemObj.text;
|
||||
if (itemObj.text && typeof itemObj.text === "object" && "text" in itemObj.text) {
|
||||
return ((itemObj.text as Record<string, unknown>).text as string | undefined) ?? "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
|
|
@ -1516,11 +1567,11 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
|
|||
}
|
||||
|
||||
if (typeof data?.completion === "string") {
|
||||
return data.completion as string;
|
||||
return data.completion;
|
||||
}
|
||||
|
||||
if (typeof data?.resultText === "string") {
|
||||
return data.resultText as string;
|
||||
return data.resultText;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ import { ChatOpenAI } from "@langchain/openai";
|
|||
export interface ChatLMStudioInput {
|
||||
modelName?: string;
|
||||
apiKey?: string;
|
||||
configuration?: any;
|
||||
configuration?: Record<string, unknown>;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
frequencyPenalty?: number;
|
||||
streaming?: boolean;
|
||||
streamUsage?: boolean;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +79,7 @@ export class ChatLMStudio extends ChatOpenAI {
|
|||
// `text: { format: undefined }` (serializes to `text: {}`) which LM Studio
|
||||
// rejects with "Required: text.format".
|
||||
modelKwargs: {
|
||||
...fields.modelKwargs,
|
||||
...(fields.modelKwargs as Record<string, unknown> | undefined),
|
||||
text: { format: { type: "text" } },
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseChatModelParams } from "@langchain/core/language_models/chat_models";
|
||||
import { AIMessageChunk } from "@langchain/core/messages";
|
||||
import { AIMessageChunk, BaseMessage } from "@langchain/core/messages";
|
||||
import type { UsageMetadata } from "@langchain/core/messages";
|
||||
import { ChatGenerationChunk } from "@langchain/core/outputs";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
|
@ -44,7 +44,12 @@ export interface ChatOpenRouterInput extends BaseChatModelParams {
|
|||
// All other ChatOpenAI parameters
|
||||
modelName?: string;
|
||||
apiKey?: string;
|
||||
configuration?: any;
|
||||
configuration?: {
|
||||
baseURL?: string;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
|
|
@ -52,7 +57,7 @@ export interface ChatOpenRouterInput extends BaseChatModelParams {
|
|||
streaming?: boolean;
|
||||
maxRetries?: number;
|
||||
maxConcurrency?: number;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class ChatOpenRouter extends ChatOpenAI {
|
||||
|
|
@ -101,7 +106,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
*
|
||||
* @see https://openrouter.ai/docs/features/prompt-caching
|
||||
*/
|
||||
override invocationParams(options?: this["ParsedCallOptions"]): any {
|
||||
override invocationParams(options?: this["ParsedCallOptions"]): Record<string, unknown> {
|
||||
const baseParams = super.invocationParams(options);
|
||||
|
||||
// Only inject cache_control for OpenRouter endpoints. LM Studio, Copilot Plus, and
|
||||
|
|
@ -150,15 +155,15 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
* LangChain filters out reasoning_details, so we bypass it completely
|
||||
*/
|
||||
override async *_streamResponseChunks(
|
||||
messages: any[],
|
||||
messages: BaseMessage[],
|
||||
options: this["ParsedCallOptions"],
|
||||
_runManager?: any
|
||||
_runManager?: { handleLLMNewToken: (token: string) => Promise<void> }
|
||||
): AsyncGenerator<ChatGenerationChunk> {
|
||||
const params = this.invocationParams(options);
|
||||
const openaiMessages = this.toOpenRouterMessages(messages);
|
||||
|
||||
const stream = (await this.openaiClient.chat.completions.create({
|
||||
...(params as Record<string, unknown>),
|
||||
...params,
|
||||
messages: openaiMessages,
|
||||
stream: true,
|
||||
stream_options: {
|
||||
|
|
@ -190,7 +195,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
|
||||
const messageChunk = this.buildMessageChunk({
|
||||
rawChunk,
|
||||
delta,
|
||||
delta: delta as unknown as Record<string, unknown>,
|
||||
content,
|
||||
finishReason: choice.finish_reason,
|
||||
reasoningDetails,
|
||||
|
|
@ -230,9 +235,13 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
* @param messages LangChain messages passed into the model
|
||||
* @returns Messages formatted for the OpenRouter API
|
||||
*/
|
||||
private toOpenRouterMessages(messages: any[]): OpenRouterMessageParam[] {
|
||||
private toOpenRouterMessages(messages: BaseMessage[]): OpenRouterMessageParam[] {
|
||||
return messages.map((msg) => {
|
||||
const role = typeof msg._getType === "function" ? msg._getType() : (msg.role ?? "user");
|
||||
const msgRecord = msg as unknown as Record<string, unknown>;
|
||||
const role =
|
||||
typeof msg._getType === "function"
|
||||
? msg._getType()
|
||||
: ((msgRecord.role as string) ?? "user");
|
||||
const mappedRole =
|
||||
role === "human"
|
||||
? "user"
|
||||
|
|
@ -240,12 +249,12 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
? "assistant"
|
||||
: (role as OpenAI.ChatCompletionRole);
|
||||
|
||||
if (msg.tool_call_id) {
|
||||
if (msgRecord.tool_call_id) {
|
||||
return {
|
||||
role: "tool",
|
||||
content: msg.content,
|
||||
tool_call_id: msg.tool_call_id,
|
||||
};
|
||||
tool_call_id: msgRecord.tool_call_id as string,
|
||||
} as OpenRouterMessageParam;
|
||||
}
|
||||
|
||||
if (msg.additional_kwargs?.function_call) {
|
||||
|
|
@ -280,7 +289,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
*/
|
||||
private buildMessageChunk(config: {
|
||||
rawChunk: OpenRouterChatChunk;
|
||||
delta: Record<string, any>;
|
||||
delta: Record<string, unknown>;
|
||||
content: string;
|
||||
finishReason: string | null | undefined;
|
||||
reasoningText?: string;
|
||||
|
|
@ -376,11 +385,14 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
* @param choice Chunk choice object from the OpenRouter stream
|
||||
* @returns Array of reasoning detail entries, if present
|
||||
*/
|
||||
private extractReasoningDetails(choice: any): unknown[] | undefined {
|
||||
private extractReasoningDetails(
|
||||
choice: OpenAI.ChatCompletionChunk.Choice
|
||||
): unknown[] | undefined {
|
||||
const choiceRecord = choice as unknown as Record<string, Record<string, unknown>>;
|
||||
const candidate =
|
||||
choice?.delta?.reasoning_details ??
|
||||
choice?.message?.reasoning_details ??
|
||||
choice?.reasoning_details;
|
||||
choiceRecord?.delta?.reasoning_details ??
|
||||
choiceRecord?.message?.reasoning_details ??
|
||||
(choice as unknown as Record<string, unknown>)?.reasoning_details;
|
||||
|
||||
if (!Array.isArray(candidate)) {
|
||||
return undefined;
|
||||
|
|
@ -428,7 +440,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
* @returns Tool call chunk array compatible with LangChain
|
||||
*/
|
||||
private extractToolCallChunks(
|
||||
toolCalls: any
|
||||
toolCalls: unknown
|
||||
):
|
||||
| Array<{ name?: string; args?: string; id?: string; index?: number; type: "tool_call_chunk" }>
|
||||
| undefined {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { OpenAIEmbeddings, type OpenAIEmbeddingsParams } from "@langchain/openai";
|
||||
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||
|
||||
export class CustomOpenAIEmbeddings extends OpenAIEmbeddings {
|
||||
private customConfig: any;
|
||||
private customConfig: Record<string, unknown>;
|
||||
|
||||
constructor(config: any) {
|
||||
super(config as OpenAIEmbeddingsParams);
|
||||
constructor(config: Record<string, unknown>) {
|
||||
super(config);
|
||||
// Store the config for our custom methods
|
||||
this.customConfig = config;
|
||||
}
|
||||
|
|
@ -29,17 +29,20 @@ export class CustomOpenAIEmbeddings extends OpenAIEmbeddings {
|
|||
};
|
||||
|
||||
// Get the correct baseURL, apiKey, and fetch function from the configuration
|
||||
const baseURL = this.customConfig.configuration?.baseURL || "https://api.openai.com/v1";
|
||||
const configuration = this.customConfig.configuration as
|
||||
| { baseURL?: string; fetch?: typeof fetch }
|
||||
| undefined;
|
||||
const baseURL = configuration?.baseURL || "https://api.openai.com/v1";
|
||||
const url = `${baseURL}/embeddings`;
|
||||
const apiKey = this.customConfig.apiKey;
|
||||
const fetchFn = this.customConfig.configuration?.fetch || fetch;
|
||||
const apiKey = this.customConfig.apiKey as string;
|
||||
const fetchFn = configuration?.fetch || fetch;
|
||||
|
||||
const response = await fetchFn(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
...(this.customConfig.headers || {}),
|
||||
...((this.customConfig.headers as Record<string, string>) || {}),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ export interface RerankResponse {
|
|||
}
|
||||
|
||||
export interface ToolCall {
|
||||
tool: any;
|
||||
args: any;
|
||||
tool: unknown;
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
export interface Url4llmResponse {
|
||||
|
|
@ -38,7 +38,7 @@ export interface Pdf4llmResponse {
|
|||
}
|
||||
|
||||
export interface Docs4llmResponse {
|
||||
response: any;
|
||||
response: unknown;
|
||||
elapsed_time_ms: number;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ export interface Youtube4llmResponse {
|
|||
}
|
||||
|
||||
export interface Twitter4llmResponse {
|
||||
response: any;
|
||||
response: string;
|
||||
elapsed_time_ms: number;
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ export class BrevilabsClient {
|
|||
|
||||
private async makeRequest<T>(
|
||||
endpoint: string,
|
||||
body: any,
|
||||
body: Record<string, unknown>,
|
||||
method = "POST",
|
||||
excludeAuthHeader = false,
|
||||
skipLicenseCheck = false
|
||||
|
|
@ -112,7 +112,7 @@ export class BrevilabsClient {
|
|||
const url = new URL(`${BREVILABS_API_BASE_URL}${endpoint}`);
|
||||
if (method === "GET") {
|
||||
// Add query parameters for GET requests
|
||||
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
|
||||
Object.entries(body).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value as string);
|
||||
});
|
||||
}
|
||||
|
|
@ -194,10 +194,10 @@ export class BrevilabsClient {
|
|||
* unknown error.
|
||||
*/
|
||||
async validateLicenseKey(
|
||||
context?: Record<string, any>
|
||||
context?: Record<string, unknown>
|
||||
): Promise<{ isValid: boolean | undefined; plan?: string }> {
|
||||
// Build the request body with proper structure
|
||||
const requestBody: Record<string, any> = {
|
||||
const requestBody: Record<string, unknown> = {
|
||||
license_key: await getDecryptedKey(getSettings().plusLicenseKey),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ type AgentSource = {
|
|||
title: string;
|
||||
path: string;
|
||||
score: number;
|
||||
explanation?: any;
|
||||
explanation?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -476,11 +476,11 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
|
||||
this.lastDisplayedContent = "";
|
||||
return loopResult.finalResponse;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// Always stop the reasoning timer on error
|
||||
this.stopReasoningTimer();
|
||||
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) {
|
||||
logInfo("Autonomous agent stream aborted by user", {
|
||||
reason: abortController.signal.reason,
|
||||
});
|
||||
|
|
@ -538,7 +538,11 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
*/
|
||||
private async prepareAgentConversation(
|
||||
userMessage: ChatMessage,
|
||||
chatModel: any,
|
||||
chatModel: BaseChatModel & {
|
||||
modelName?: string;
|
||||
model?: string;
|
||||
bindTools?: (tools: unknown[]) => unknown;
|
||||
},
|
||||
_updateLoadingMessage?: (message: string) => void // Unused, kept for potential future use
|
||||
): Promise<AgentRunContext> {
|
||||
const messages: BaseMessage[] = [];
|
||||
|
|
@ -622,7 +626,7 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
// Extract user content (L3 smart references + L5) from base messages
|
||||
const userMessageContent = baseMessages.find((m) => m.role === "user");
|
||||
if (userMessageContent) {
|
||||
const isMultimodal = this.isMultimodalModel(chatModel as BaseChatModel);
|
||||
const isMultimodal = this.isMultimodalModel(chatModel);
|
||||
const content: string | MessageContent[] = isMultimodal
|
||||
? await this.buildMessageContent(userMessageContent.content, userMessage)
|
||||
: userMessageContent.content;
|
||||
|
|
@ -713,7 +717,8 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
// 2. Model returned only thinking/reasoning content that gets filtered
|
||||
let finalContent = content;
|
||||
if (!finalContent || finalContent.trim() === "") {
|
||||
const rawToolCallChunks = (aiMessage as any).tool_call_chunks ?? [];
|
||||
const rawToolCallChunks =
|
||||
(aiMessage as { tool_call_chunks?: unknown[] }).tool_call_chunks ?? [];
|
||||
logWarn(
|
||||
`[Agent] Empty response detected (iteration ${iteration}). ` +
|
||||
`Content length: ${content?.length ?? 0}, ` +
|
||||
|
|
@ -1048,7 +1053,7 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
if (chunkContent) rawContent += chunkContent;
|
||||
|
||||
// Process chunk through ThinkBlockStreamer to strip thinking content
|
||||
thinkStreamer.processChunk(chunk);
|
||||
thinkStreamer.processChunk(chunk as Parameters<typeof thinkStreamer.processChunk>[0]);
|
||||
}
|
||||
|
||||
// Close the streamer to finalize content (handles unclosed think blocks, etc.)
|
||||
|
|
@ -1083,9 +1088,9 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
aiMessage,
|
||||
streamingResult,
|
||||
};
|
||||
} catch (error: any) {
|
||||
logError(`Stream error: ${error.message}`);
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
} catch (error: unknown) {
|
||||
logError(`Stream error: ${(error as Error).message}`);
|
||||
if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) {
|
||||
const streamingResult = thinkStreamer.close();
|
||||
return {
|
||||
content: streamingResult.content,
|
||||
|
|
|
|||
|
|
@ -110,8 +110,9 @@ export abstract class BaseChainRunner implements ChainRunner {
|
|||
updateCurrentAiMessage("");
|
||||
}
|
||||
// Log compact memory summary and a truncated final response (~300 chars)
|
||||
const historyMessages = (this.chainManager.memoryManager.getMemory().chatHistory as any)
|
||||
.messages;
|
||||
const historyMessages = (
|
||||
this.chainManager.memoryManager.getMemory().chatHistory as { messages?: unknown[] }
|
||||
).messages;
|
||||
logInfo("Chat memory updated:\n", {
|
||||
turns: Array.isArray(historyMessages) ? historyMessages.length : 0,
|
||||
});
|
||||
|
|
@ -120,8 +121,8 @@ export abstract class BaseChainRunner implements ChainRunner {
|
|||
try {
|
||||
const { parseToolCallMarkers } = await import("./utils/toolCallParser");
|
||||
const parsed = parseToolCallMarkers(fullAIResponse);
|
||||
let textOnly = parsed.segments
|
||||
.map((seg: { type: string; content: string }) => (seg.type === "text" ? seg.content : ""))
|
||||
let textOnly = (parsed.segments as { type: string; content: string }[])
|
||||
.map((seg) => (seg.type === "text" ? seg.content : ""))
|
||||
.join("")
|
||||
.trim();
|
||||
if (!textOnly) textOnly = fullAIResponse || "";
|
||||
|
|
@ -146,15 +147,16 @@ export abstract class BaseChainRunner implements ChainRunner {
|
|||
* @param error - Raw provider error object.
|
||||
* @param processErrorChunk - Callback used to stream error text to the UI.
|
||||
*/
|
||||
protected async handleError(error: any, processErrorChunk: (message: string) => void) {
|
||||
protected async handleError(error: unknown, processErrorChunk: (message: string) => void) {
|
||||
const msg = err2String(error);
|
||||
logError("Error during LLM invocation:", msg);
|
||||
const errorData = error?.response?.data?.error || msg;
|
||||
const errorCode = errorData?.code || msg;
|
||||
const errorData =
|
||||
(error as { response?: { data?: { error?: unknown } } })?.response?.data?.error || msg;
|
||||
const errorCode = (errorData as { code?: string })?.code || msg;
|
||||
let errorMessage = "";
|
||||
|
||||
// Check for specific error messages
|
||||
if (error?.message?.includes("Invalid license key")) {
|
||||
if ((error as { message?: string })?.message?.includes("Invalid license key")) {
|
||||
errorMessage = "Invalid Copilot Plus license key. Please check your license key in settings.";
|
||||
} else if (errorCode === "model_not_found") {
|
||||
errorMessage =
|
||||
|
|
@ -200,7 +202,16 @@ export abstract class BaseChainRunner implements ChainRunner {
|
|||
* @returns True if the error indicates an authentication problem.
|
||||
*/
|
||||
private isAuthenticationError(error: unknown, normalizedMessage: string): boolean {
|
||||
const responseError = (error as { response?: { status?: number; data?: any } })?.response;
|
||||
const responseError = (
|
||||
error as {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
error?: { status?: number | string; code?: string; message?: string; type?: string };
|
||||
};
|
||||
};
|
||||
}
|
||||
)?.response;
|
||||
const errorData = responseError?.data?.error ?? (error as { error?: unknown })?.error;
|
||||
const rawStatus = responseError?.status ?? (errorData as { status?: number | string })?.status;
|
||||
const statusCode = typeof rawStatus === "string" ? Number.parseInt(rawStatus, 10) : rawStatus;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
isFilterOnlyResults,
|
||||
isTimeDominantResults,
|
||||
logSearchResultsDebugTable,
|
||||
type SearchDoc,
|
||||
} from "./utils/searchResultUtils";
|
||||
import {
|
||||
buildLocalSearchInnerContent,
|
||||
|
|
@ -68,8 +69,8 @@ import ProjectManager from "@/LLMProviders/projectManager";
|
|||
import { isProjectMode } from "@/aiParams";
|
||||
|
||||
type ToolCallWithExecutor = {
|
||||
tool: any;
|
||||
args: any;
|
||||
tool: StructuredTool;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export class CopilotPlusChainRunner extends BaseChainRunner {
|
||||
|
|
@ -113,7 +114,10 @@ export class CopilotPlusChainRunner extends BaseChainRunner {
|
|||
const availableTools = this.getAvailableToolsForPlanning();
|
||||
|
||||
// Check if model supports native tool calling
|
||||
if (typeof (chatModel as any).bindTools !== "function") {
|
||||
const modelWithTools = chatModel as BaseChatModel & {
|
||||
bindTools?: (tools: StructuredTool[]) => unknown;
|
||||
};
|
||||
if (typeof modelWithTools.bindTools !== "function") {
|
||||
logWarn("[CopilotPlus] Model does not support native tool calling, skipping tool planning");
|
||||
return {
|
||||
toolCalls: [],
|
||||
|
|
@ -122,7 +126,7 @@ export class CopilotPlusChainRunner extends BaseChainRunner {
|
|||
}
|
||||
|
||||
// Bind tools to the model for native function calling
|
||||
const boundModel = (chatModel as any).bindTools(availableTools);
|
||||
const boundModel = modelWithTools.bindTools(availableTools);
|
||||
|
||||
// Build a lightweight planning prompt (no XML format instructions needed)
|
||||
const planningPrompt = `You are a helpful AI assistant. Analyze the user's message and determine if any tools should be called.
|
||||
|
|
@ -160,8 +164,10 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
// token estimation entirely. Actual token usage comes from API response metadata.
|
||||
let response: AIMessage;
|
||||
{
|
||||
const stream: AsyncIterable<AIMessageChunk> = await withSuppressedTokenWarnings(
|
||||
() => boundModel.stream(planningMessages) as Promise<AsyncIterable<AIMessageChunk>>
|
||||
const stream: AsyncIterable<AIMessageChunk> = await withSuppressedTokenWarnings(() =>
|
||||
(
|
||||
boundModel as { stream: (msgs: unknown) => Promise<AsyncIterable<AIMessageChunk>> }
|
||||
).stream(planningMessages)
|
||||
);
|
||||
let aggregated: AIMessageChunk | undefined;
|
||||
for await (const chunk of stream) {
|
||||
|
|
@ -248,7 +254,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
private async processAtCommands(
|
||||
userMessage: string,
|
||||
existingToolCalls: ToolCallWithExecutor[],
|
||||
context: { salientTerms: string[]; timeRange?: any }
|
||||
context: { salientTerms: string[]; timeRange?: unknown }
|
||||
): Promise<ToolCallWithExecutor[]> {
|
||||
const message = userMessage.toLowerCase();
|
||||
const cleanQuery = this.removeAtCommands(userMessage);
|
||||
|
|
@ -546,8 +552,8 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
}
|
||||
|
||||
protected hasCapability(model: BaseChatModel, capability: ModelCapability): boolean {
|
||||
const modelName: string =
|
||||
((model as any).modelName as string) || ((model as any).model as string) || "";
|
||||
const modelWithName = model as BaseChatModel & { modelName?: string; model?: string };
|
||||
const modelName: string = modelWithName.modelName || modelWithName.model || "";
|
||||
const customModel = this.chainManager.chatModelManager.findModelByName(modelName);
|
||||
return customModel?.capabilities?.includes(capability) ?? false;
|
||||
}
|
||||
|
|
@ -571,7 +577,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
private async streamMultimodalResponse(
|
||||
textContent: string,
|
||||
userMessage: ChatMessage,
|
||||
allToolOutputs: any[],
|
||||
allToolOutputs: { tool: string; output: unknown }[],
|
||||
abortController: AbortController,
|
||||
thinkStreamer: ThinkBlockStreamer,
|
||||
originalUserQuestion: string,
|
||||
|
|
@ -585,7 +591,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
const isMultimodalCurrent = this.isMultimodalModel(chatModel);
|
||||
|
||||
// Create messages array
|
||||
const messages: { role: string; content: any }[] = [];
|
||||
const messages: { role: string; content: string | MessageContent[] }[] = [];
|
||||
|
||||
// Envelope-based context construction (required)
|
||||
const envelope = userMessage.contextEnvelope;
|
||||
|
|
@ -714,7 +720,9 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
});
|
||||
break;
|
||||
}
|
||||
for await (const processedChunk of actionStreamer.processChunk(chunk)) {
|
||||
for await (const processedChunk of actionStreamer.processChunk(
|
||||
chunk as unknown as Record<string, unknown>
|
||||
)) {
|
||||
thinkStreamer.processChunk(processedChunk);
|
||||
}
|
||||
}
|
||||
|
|
@ -740,7 +748,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
const excludeThinking = !hasReasoning;
|
||||
|
||||
const thinkStreamer = new ThinkBlockStreamer(updateCurrentAiMessage, excludeThinking);
|
||||
let sources: { title: string; path: string; score: number; explanation?: any }[] = [];
|
||||
let sources: { title: string; path: string; score: number; explanation?: unknown }[] = [];
|
||||
|
||||
const isPlusUser = await checkIsPlusUser({
|
||||
isCopilotPlus: true,
|
||||
|
|
@ -783,7 +791,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
|
||||
// Execute getTimeRangeMs immediately if present (needed for localSearch timeRange)
|
||||
// We execute it once here and remove it from toolCalls to avoid double execution
|
||||
let timeRange: any = undefined;
|
||||
let timeRange: unknown = undefined;
|
||||
const timeRangeCall = planningResult.toolCalls.find(
|
||||
(tc) => tc.tool.name === "getTimeRangeMs"
|
||||
);
|
||||
|
|
@ -794,7 +802,12 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
);
|
||||
// Parse result if it's a JSON string (LangChain tools return strings)
|
||||
// Extract epoch values from TimeInfo objects - localSearch expects {startTime: number, endTime: number}
|
||||
const extractEpochValues = (result: any): unknown => {
|
||||
type TimeInfoResult = {
|
||||
startTime?: { epoch?: number };
|
||||
endTime?: { epoch?: number };
|
||||
error?: unknown;
|
||||
};
|
||||
const extractEpochValues = (result: TimeInfoResult): unknown => {
|
||||
if (result?.startTime?.epoch !== undefined && result?.endTime?.epoch !== undefined) {
|
||||
return {
|
||||
startTime: result.startTime.epoch,
|
||||
|
|
@ -806,7 +819,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
|
||||
if (typeof timeRangeResult === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(timeRangeResult);
|
||||
const parsed = JSON.parse(timeRangeResult) as TimeInfoResult;
|
||||
// Only use result if it's not an error
|
||||
if (!parsed.error) {
|
||||
timeRange = extractEpochValues(parsed);
|
||||
|
|
@ -814,8 +827,11 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
} catch {
|
||||
logWarn("[CopilotPlus] Failed to parse getTimeRangeMs result:", timeRangeResult);
|
||||
}
|
||||
} else if (timeRangeResult && !timeRangeResult.error) {
|
||||
timeRange = extractEpochValues(timeRangeResult);
|
||||
} else if (timeRangeResult) {
|
||||
const typedResult = timeRangeResult as TimeInfoResult;
|
||||
if (!typedResult.error) {
|
||||
timeRange = extractEpochValues(typedResult);
|
||||
}
|
||||
}
|
||||
logInfo("[CopilotPlus] Executed getTimeRangeMs, result:", timeRange);
|
||||
}
|
||||
|
|
@ -839,7 +855,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
salientTerms: planningResult.salientTerms,
|
||||
timeRange,
|
||||
});
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return this.handleResponse(
|
||||
getApiErrorMessage(error),
|
||||
userMessage,
|
||||
|
|
@ -887,12 +903,15 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
cleanedUserMessage,
|
||||
updateLoadingMessage
|
||||
);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// Reset loading message to default
|
||||
updateLoadingMessage?.(LOADING_MESSAGES.DEFAULT);
|
||||
|
||||
// Check if the error is due to abort signal
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
if (
|
||||
(error instanceof Error && error.name === "AbortError") ||
|
||||
abortController.signal.aborted
|
||||
) {
|
||||
logInfo("CopilotPlus stream aborted by user", { reason: abortController.signal.reason });
|
||||
// Don't show error message for user-initiated aborts
|
||||
} else {
|
||||
|
|
@ -924,7 +943,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
const fallbackSources =
|
||||
this.lastCitationSources && this.lastCitationSources.length > 0
|
||||
? this.lastCitationSources
|
||||
: ((sources as any[]) || []).map((source) => ({ title: source.title, path: source.path }));
|
||||
: (sources || []).map((source) => ({ title: source.title, path: source.path }));
|
||||
|
||||
fullAIResponse = addFallbackSources(
|
||||
fullAIResponse,
|
||||
|
|
@ -947,14 +966,14 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
}
|
||||
|
||||
private async executeToolCalls(
|
||||
toolCalls: any[],
|
||||
toolCalls: ToolCallWithExecutor[],
|
||||
updateLoadingMessage?: (message: string) => void
|
||||
): Promise<{
|
||||
toolOutputs: { tool: string; output: any }[];
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[];
|
||||
toolOutputs: { tool: string; output: unknown }[];
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[];
|
||||
}> {
|
||||
const toolOutputs = [];
|
||||
const allSources: { title: string; path: string; score: number; explanation?: any }[] = [];
|
||||
const toolOutputs: { tool: string; output: unknown }[] = [];
|
||||
const allSources: { title: string; path: string; score: number; explanation?: unknown }[] = [];
|
||||
|
||||
// TODO: remove this hack until better solution in place (logan, wenzheng)
|
||||
// Skip getFileTree if localSearch is already being called to avoid redundant work
|
||||
|
|
@ -1002,17 +1021,32 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
// Persist citation lines built for this turn to reuse in fallback
|
||||
private lastCitationSources: { title?: string; path?: string }[] | null = null;
|
||||
|
||||
protected getTimeExpression(toolCalls: any[]): string {
|
||||
protected getTimeExpression(toolCalls: ToolCallWithExecutor[]): string {
|
||||
const timeRangeCall = toolCalls.find((call) => call.tool.name === "getTimeRangeMs");
|
||||
return timeRangeCall ? (timeRangeCall.args.timeExpression as string) : "";
|
||||
}
|
||||
|
||||
private prepareLocalSearchResult(documents: any[], timeExpression: string): string {
|
||||
private prepareLocalSearchResult(documents: unknown[], timeExpression: string): string {
|
||||
const settings = getSettings();
|
||||
|
||||
// Type alias for the shape we need from search result documents
|
||||
type SearchDoc = {
|
||||
includeInContext?: boolean;
|
||||
mtime?: number;
|
||||
content?: string;
|
||||
source?: string;
|
||||
isFilterResult?: boolean;
|
||||
title?: string;
|
||||
path?: string;
|
||||
__sourceId?: number;
|
||||
};
|
||||
|
||||
// Cast once to a typed array we can work with throughout this method
|
||||
const typedDocs = documents as SearchDoc[];
|
||||
|
||||
// Filter documents that should be included in context
|
||||
// Use !== false to be consistent with formatSearchResultsForLLM and logSearchResultsDebugTable
|
||||
const includedDocs = documents.filter((doc) => doc.includeInContext !== false);
|
||||
const includedDocs = typedDocs.filter((doc) => doc.includeInContext !== false);
|
||||
|
||||
// Generate quality summary across all docs combined
|
||||
const qualitySummary = generateQualitySummary(includedDocs);
|
||||
|
|
@ -1022,12 +1056,12 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
// timeDominant is checked independently of filterOnly: time-range queries often include
|
||||
// daily notes with source "title-match" (from getTitleMatches), which are not in
|
||||
// FILTER_SOURCES, so filterOnly would be false even for pure time-range result sets.
|
||||
const filterOnly = isFilterOnlyResults(includedDocs as Array<{ source?: string }>);
|
||||
const timeDominant = isTimeDominantResults(includedDocs as Array<{ source?: string }>);
|
||||
const filterOnly = isFilterOnlyResults(includedDocs);
|
||||
const timeDominant = isTimeDominantResults(includedDocs);
|
||||
|
||||
// Determine tier split based on result type
|
||||
let tier1Docs: any[];
|
||||
let tier2Docs: any[];
|
||||
let tier1Docs: SearchDoc[];
|
||||
let tier2Docs: SearchDoc[];
|
||||
if (timeDominant) {
|
||||
// Sort by mtime desc; most recent get full content, older get metadata-only
|
||||
const sorted = [...includedDocs].sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
|
||||
|
|
@ -1049,36 +1083,39 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
}
|
||||
|
||||
// Calculate total content length for tier 1 only (safety net truncation)
|
||||
const totalContentLength = tier1Docs.reduce(
|
||||
(sum, doc): number => sum + ((doc.content?.length as number) || 0),
|
||||
0
|
||||
);
|
||||
const totalContentLength = tier1Docs.reduce<number>((sum, doc) => {
|
||||
return sum + (doc.content ? doc.content.length : 0);
|
||||
}, 0);
|
||||
|
||||
// If total content length exceeds threshold, truncate content proportionally
|
||||
let processedDocs = tier1Docs;
|
||||
let processedDocs: SearchDoc[] = tier1Docs;
|
||||
if (totalContentLength > MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT) {
|
||||
const truncationRatio = MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT / totalContentLength;
|
||||
logInfo(
|
||||
"Truncating document contents to fit context length. Truncation ratio:",
|
||||
truncationRatio
|
||||
);
|
||||
processedDocs = tier1Docs.map((doc): any => ({
|
||||
...doc,
|
||||
content:
|
||||
doc.content?.slice(0, Math.floor((doc.content?.length || 0) * truncationRatio)) || "",
|
||||
}));
|
||||
processedDocs = tier1Docs.map(
|
||||
(doc): SearchDoc => ({
|
||||
...doc,
|
||||
content:
|
||||
doc.content?.slice(0, Math.floor((doc.content?.length || 0) * truncationRatio)) || "",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Assign stable source ids (continuous across both groups) and sanitize content
|
||||
const withIds = processedDocs.map((doc, idx): any => ({
|
||||
...doc,
|
||||
__sourceId: idx + 1,
|
||||
content: sanitizeContentForCitations((doc.content as string) || ""),
|
||||
}));
|
||||
const withIds: SearchDoc[] = processedDocs.map(
|
||||
(doc, idx): SearchDoc => ({
|
||||
...doc,
|
||||
__sourceId: idx + 1,
|
||||
content: sanitizeContentForCitations((doc.content as string) || ""),
|
||||
})
|
||||
);
|
||||
|
||||
// Split into filter and search docs by isFilterResult flag
|
||||
const filterDocs = withIds.filter((d: any) => d.isFilterResult === true);
|
||||
const searchDocs = withIds.filter((d: any) => d.isFilterResult !== true);
|
||||
const filterDocs = withIds.filter((d) => d.isFilterResult === true);
|
||||
const searchDocs = withIds.filter((d) => d.isFilterResult !== true);
|
||||
|
||||
// Use split formatter if there are filter results, otherwise fall back to unified format.
|
||||
// For tag-only queries (tier1 empty), output metadata-only directly.
|
||||
|
|
@ -1108,14 +1145,14 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
// Build a compact, unnumbered source catalog to avoid bias
|
||||
const sourceEntries: SourceCatalogEntry[] = withIds
|
||||
.slice(0, Math.min(20, withIds.length))
|
||||
.map((d: any) => ({
|
||||
.map((d) => ({
|
||||
title: d.title || d.path || "Untitled",
|
||||
path: d.path || d.title || "",
|
||||
}));
|
||||
const catalogLines = formatSourceCatalog(sourceEntries);
|
||||
|
||||
// Also keep a numbered mapping for fallback use only (if model emits footnotes but forgets Sources)
|
||||
this.lastCitationSources = withIds.slice(0, Math.min(20, withIds.length)).map((d: any) => {
|
||||
this.lastCitationSources = withIds.slice(0, Math.min(20, withIds.length)).map((d) => {
|
||||
const title = d.title || d.path || "Untitled";
|
||||
return {
|
||||
title,
|
||||
|
|
@ -1151,9 +1188,9 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
): {
|
||||
formattedForLLM: string;
|
||||
formattedForDisplay: string;
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[];
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[];
|
||||
} {
|
||||
let sources: { title: string; path: string; score: number; explanation?: any }[] = [];
|
||||
let sources: { title: string; path: string; score: number; explanation?: unknown }[] = [];
|
||||
let formattedForLLM: string;
|
||||
let formattedForDisplay: string;
|
||||
|
||||
|
|
@ -1179,7 +1216,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
}
|
||||
|
||||
// Log a concise debug table of results with explanations (title, ctime, mtime)
|
||||
logSearchResultsDebugTable(searchResults);
|
||||
logSearchResultsDebugTable(searchResults as SearchDoc[]);
|
||||
|
||||
// Extract sources with explanation for UI display
|
||||
sources = extractSourcesFromSearchResults(searchResults);
|
||||
|
|
@ -1208,15 +1245,13 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
* Formats all tool outputs uniformly for user message.
|
||||
* All tools (localSearch, webSearch, getFileTree, etc.) are treated the same.
|
||||
*/
|
||||
private formatAllToolOutputs(toolOutputs: any[]): string {
|
||||
private formatAllToolOutputs(toolOutputs: { tool: string; output: unknown }[]): string {
|
||||
if (toolOutputs.length === 0) return "";
|
||||
|
||||
const formattedOutputs = toolOutputs
|
||||
.map((output) => {
|
||||
let content = output.output;
|
||||
if (typeof content !== "string") {
|
||||
content = JSON.stringify(content);
|
||||
}
|
||||
const content: string =
|
||||
typeof output.output === "string" ? output.output : JSON.stringify(output.output);
|
||||
return `<${output.tool}>\n${content}\n</${output.tool}>`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ export class LLMChainRunner extends BaseChainRunner {
|
|||
* Construct messages array using envelope-based context (L1-L5 layers)
|
||||
* Requires context envelope - throws error if unavailable
|
||||
*/
|
||||
private async constructMessages(userMessage: ChatMessage): Promise<any[]> {
|
||||
private async constructMessages(
|
||||
userMessage: ChatMessage
|
||||
): Promise<{ role: string; content: string | unknown[] }[]> {
|
||||
// Require envelope for LLM chain
|
||||
if (!userMessage.contextEnvelope) {
|
||||
throw new Error(
|
||||
|
|
@ -32,7 +34,7 @@ export class LLMChainRunner extends BaseChainRunner {
|
|||
debug: false,
|
||||
});
|
||||
|
||||
const messages: { role: string; content: any }[] = [];
|
||||
const messages: { role: string; content: string | unknown[] }[] = [];
|
||||
|
||||
// Add system message (L1)
|
||||
const systemMessage = baseMessages.find((m) => m.role === "system");
|
||||
|
|
@ -115,9 +117,13 @@ export class LLMChainRunner extends BaseChainRunner {
|
|||
|
||||
// Stream with abort signal
|
||||
const chatStream = await withSuppressedTokenWarnings(() =>
|
||||
this.chainManager.chatModelManager.getChatModel().stream(messages, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
this.chainManager.chatModelManager.getChatModel().stream(
|
||||
// ProviderMessage[] format matches what getChatModel().stream() accepts at runtime
|
||||
messages as never,
|
||||
{
|
||||
signal: abortController.signal,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
for await (const chunk of chatStream) {
|
||||
|
|
@ -125,7 +131,7 @@ export class LLMChainRunner extends BaseChainRunner {
|
|||
logInfo("Stream iteration aborted", { reason: abortController.signal.reason });
|
||||
break;
|
||||
}
|
||||
streamer.processChunk(chunk);
|
||||
streamer.processChunk(chunk as Parameters<typeof streamer.processChunk>[0]);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Check if the error is due to abort signal
|
||||
|
|
|
|||
|
|
@ -138,22 +138,26 @@ export class VaultQAChainRunner extends BaseChainRunner {
|
|||
// Format documents as context with XML tags
|
||||
// Sanitize content to remove pre-existing citation markers
|
||||
|
||||
const context = retrievedDocs
|
||||
.map((doc: any) => {
|
||||
const context = (
|
||||
retrievedDocs as { metadata?: { title?: string; path?: string }; pageContent?: string }[]
|
||||
)
|
||||
.map((doc) => {
|
||||
const title = doc.metadata?.title || "Untitled";
|
||||
const path = doc.metadata?.path || title;
|
||||
return `<${RETRIEVED_DOCUMENT_TAG}>\n<title>${title}</title>\n<path>${path}</path>\n<content>\n${sanitizeContentForCitations(doc.pageContent as string)}\n</content>\n</${RETRIEVED_DOCUMENT_TAG}>`;
|
||||
return `<${RETRIEVED_DOCUMENT_TAG}>\n<title>${title}</title>\n<path>${path}</path>\n<content>\n${sanitizeContentForCitations(doc.pageContent ?? "")}\n</content>\n</${RETRIEVED_DOCUMENT_TAG}>`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
// Step 6: Build messages array with envelope-aware logic
|
||||
const messages: any[] = [];
|
||||
const messages: { role: string; content: string | unknown[] }[] = [];
|
||||
const chatModel = this.chainManager.chatModelManager.getChatModel();
|
||||
|
||||
// Prepare RAG context and citation instructions
|
||||
const sourceEntries: SourceCatalogEntry[] = retrievedDocs
|
||||
const sourceEntries: SourceCatalogEntry[] = (
|
||||
retrievedDocs as { metadata?: { title?: string; path?: string } }[]
|
||||
)
|
||||
.slice(0, Math.max(5, Math.min(20, retrievedDocs.length)))
|
||||
.map((d: any) => ({
|
||||
.map((d) => ({
|
||||
title: d.metadata?.title || d.metadata?.path || "Untitled",
|
||||
path: d.metadata?.path || d.metadata?.title || "",
|
||||
}));
|
||||
|
|
@ -190,7 +194,7 @@ export class VaultQAChainRunner extends BaseChainRunner {
|
|||
}
|
||||
|
||||
// Insert L4 (chat history) between system and user
|
||||
await loadAndAddChatHistory(memory, messages as { role: string; content: any }[]);
|
||||
await loadAndAddChatHistory(memory, messages);
|
||||
|
||||
// Add user message with RAG prepended
|
||||
// User message now contains: RAG results + citations + L3 smart references + L5
|
||||
|
|
@ -204,12 +208,14 @@ export class VaultQAChainRunner extends BaseChainRunner {
|
|||
|
||||
// Handle multimodal content if present
|
||||
if (userMessage.content && Array.isArray(userMessage.content)) {
|
||||
const updatedContent = userMessage.content.map((item: any): any => {
|
||||
if (item.type === "text") {
|
||||
return { ...item, text: enhancedUserContent };
|
||||
const updatedContent = userMessage.content.map(
|
||||
(item: { type?: string }): { type?: string; [key: string]: unknown } => {
|
||||
if (item.type === "text") {
|
||||
return { ...item, text: enhancedUserContent };
|
||||
}
|
||||
return { ...item };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
);
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: updatedContent,
|
||||
|
|
@ -234,9 +240,13 @@ export class VaultQAChainRunner extends BaseChainRunner {
|
|||
|
||||
// Stream with abort signal
|
||||
const chatStream = await withSuppressedTokenWarnings(() =>
|
||||
this.chainManager.chatModelManager.getChatModel().stream(messages, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
this.chainManager.chatModelManager.getChatModel().stream(
|
||||
// ProviderMessage[] format matches what getChatModel().stream() accepts at runtime
|
||||
messages as never,
|
||||
{
|
||||
signal: abortController.signal,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
for await (const chunk of chatStream) {
|
||||
|
|
@ -244,11 +254,11 @@ export class VaultQAChainRunner extends BaseChainRunner {
|
|||
logInfo("VaultQA stream iteration aborted", { reason: abortController.signal.reason });
|
||||
break;
|
||||
}
|
||||
streamer.processChunk(chunk);
|
||||
streamer.processChunk(chunk as Parameters<typeof streamer.processChunk>[0]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// Check if the error is due to abort signal
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) {
|
||||
logInfo("VaultQA stream aborted by user", { reason: abortController.signal.reason });
|
||||
// Don't show error message for user-initiated aborts
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const MockedToolManager = ToolManager as jest.Mocked<typeof ToolManager>;
|
|||
const MockedToolResultFormatter = ToolResultFormatter as jest.Mocked<typeof ToolResultFormatter>;
|
||||
|
||||
describe("ActionBlockStreamer", () => {
|
||||
let writeFileTool: any;
|
||||
let writeFileTool: unknown;
|
||||
let streamer: ActionBlockStreamer;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -24,8 +24,8 @@ describe("ActionBlockStreamer", () => {
|
|||
});
|
||||
|
||||
// Helper function to process chunks and collect results
|
||||
async function processChunks(chunks: { content: string | null }[]): Promise<any[]> {
|
||||
const outputContents: any[] = [];
|
||||
async function processChunks(chunks: { content: string | null }[]): Promise<unknown[]> {
|
||||
const outputContents: unknown[] = [];
|
||||
for (const chunk of chunks) {
|
||||
for await (const result of streamer.processChunk(chunk)) {
|
||||
// Always push the content, even if it's null, undefined, or empty string
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class ActionBlockStreamer {
|
|||
|
||||
constructor(
|
||||
private toolManager: typeof ToolManager,
|
||||
private writeFileTool: any
|
||||
private writeFileTool: unknown
|
||||
) {}
|
||||
|
||||
private findCompleteBlock(str: string) {
|
||||
|
|
@ -32,21 +32,23 @@ export class ActionBlockStreamer {
|
|||
};
|
||||
}
|
||||
|
||||
async *processChunk(chunk: any): AsyncGenerator<any, void, unknown> {
|
||||
async *processChunk(
|
||||
chunk: Record<string, unknown>
|
||||
): AsyncGenerator<Record<string, unknown>, void, unknown> {
|
||||
// Handle different chunk formats
|
||||
let chunkContent = "";
|
||||
|
||||
// Handle Claude thinking model array-based content
|
||||
if (Array.isArray(chunk.content)) {
|
||||
for (const item of chunk.content) {
|
||||
for (const item of chunk.content as Array<{ type?: string; text?: unknown }>) {
|
||||
if (item.type === "text" && item.text != null) {
|
||||
chunkContent += item.text;
|
||||
chunkContent += typeof item.text === "string" ? item.text : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle standard string content
|
||||
else if (chunk.content != null) {
|
||||
chunkContent = chunk.content;
|
||||
chunkContent = typeof chunk.content === "string" ? chunk.content : "";
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
|
|
@ -79,8 +81,8 @@ export class ActionBlockStreamer {
|
|||
// Format tool result using ToolResultFormatter for consistency with agent mode
|
||||
const formattedResult = ToolResultFormatter.format("writeFile", result as string);
|
||||
yield { ...chunk, content: `\n${formattedResult}\n` };
|
||||
} catch (err: any) {
|
||||
yield { ...chunk, content: `\nError: ${err?.message || err}\n` };
|
||||
} catch (err: unknown) {
|
||||
yield { ...chunk, content: `\nError: ${(err as Error)?.message ?? String(err)}\n` };
|
||||
}
|
||||
|
||||
// Remove processed block from buffer
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export class ThinkBlockStreamer {
|
|||
}
|
||||
}
|
||||
|
||||
private handleClaudeChunk(content: any[]) {
|
||||
private handleClaudeChunk(content: Array<{ type?: string; text?: string; thinking?: string }>) {
|
||||
let textContent = "";
|
||||
let hasThinkingContent = false;
|
||||
for (const item of content) {
|
||||
|
|
@ -157,10 +157,13 @@ export class ThinkBlockStreamer {
|
|||
return hasThinkingContent;
|
||||
}
|
||||
|
||||
private handleDeepseekChunk(chunk: any) {
|
||||
private handleDeepseekChunk(chunk: {
|
||||
content?: string;
|
||||
additional_kwargs?: { reasoning_content?: string };
|
||||
}) {
|
||||
// Handle standard string content
|
||||
if (typeof chunk.content === "string") {
|
||||
this.fullResponse += stripSpecialTokens(chunk.content as string);
|
||||
this.fullResponse += stripSpecialTokens(chunk.content);
|
||||
}
|
||||
|
||||
// Handle deepseek reasoning/thinking content
|
||||
|
|
@ -200,7 +203,13 @@ export class ThinkBlockStreamer {
|
|||
* - Models that only populate reasoning_details (without delta.reasoning) won't show thinking
|
||||
* - This is acceptable for now as most models use delta.reasoning for streaming
|
||||
*/
|
||||
private handleOpenRouterChunk(chunk: any) {
|
||||
private handleOpenRouterChunk(chunk: {
|
||||
content?: string;
|
||||
additional_kwargs?: {
|
||||
delta?: { reasoning?: string };
|
||||
reasoning_details?: unknown[];
|
||||
};
|
||||
}) {
|
||||
// Only process delta.reasoning (streaming), ignore reasoning_details entirely
|
||||
if (chunk.additional_kwargs?.delta?.reasoning) {
|
||||
// Skip thinking content if excludeThinking is enabled
|
||||
|
|
@ -233,7 +242,14 @@ export class ThinkBlockStreamer {
|
|||
* Accumulate native tool call chunks during streaming.
|
||||
* LangChain providers send tool_call_chunks with incremental data.
|
||||
*/
|
||||
private handleToolCallChunks(chunk: any) {
|
||||
private handleToolCallChunks(chunk: {
|
||||
tool_call_chunks?: Array<{
|
||||
index?: number;
|
||||
id?: string;
|
||||
name?: string;
|
||||
args?: string;
|
||||
}>;
|
||||
}) {
|
||||
// Check for tool_call_chunks in the chunk (LangChain streaming format)
|
||||
const toolCallChunks = chunk.tool_call_chunks;
|
||||
if (!toolCallChunks || !Array.isArray(toolCallChunks)) {
|
||||
|
|
@ -253,7 +269,17 @@ export class ThinkBlockStreamer {
|
|||
}
|
||||
}
|
||||
|
||||
processChunk(chunk: any) {
|
||||
processChunk(chunk: {
|
||||
response_metadata?: Record<string, unknown>;
|
||||
usage_metadata?: { input_tokens?: number; output_tokens?: number; total_tokens?: number };
|
||||
tool_call_chunks?: Array<{ index?: number; id?: string; name?: string; args?: string }>;
|
||||
content?: string | Array<{ type?: string; text?: string; thinking?: string }>;
|
||||
additional_kwargs?: {
|
||||
reasoning_content?: string;
|
||||
delta?: { reasoning?: string };
|
||||
reasoning_details?: unknown[];
|
||||
};
|
||||
}) {
|
||||
// Detect truncation using multi-provider detector
|
||||
const truncationResult = detectTruncation(chunk);
|
||||
if (truncationResult.wasTruncated) {
|
||||
|
|
@ -290,16 +316,17 @@ export class ThinkBlockStreamer {
|
|||
// Route based on the actual chunk format
|
||||
if (Array.isArray(chunk.content)) {
|
||||
// Claude format with content array
|
||||
this.handleClaudeChunk(chunk.content as any[]);
|
||||
// chunk.content is Array<...> in this branch (checked by Array.isArray guard above)
|
||||
this.handleClaudeChunk(chunk.content);
|
||||
} else if (chunk.additional_kwargs?.reasoning_content) {
|
||||
// Deepseek format with reasoning_content
|
||||
this.handleDeepseekChunk(chunk);
|
||||
this.handleDeepseekChunk(chunk as Parameters<typeof this.handleDeepseekChunk>[0]);
|
||||
} else if (isThinkingChunk) {
|
||||
// OpenRouter format with delta.reasoning or reasoning_details
|
||||
this.handleOpenRouterChunk(chunk);
|
||||
this.handleOpenRouterChunk(chunk as Parameters<typeof this.handleOpenRouterChunk>[0]);
|
||||
} else {
|
||||
// Default case: regular content or other formats
|
||||
this.handleDeepseekChunk(chunk);
|
||||
this.handleDeepseekChunk(chunk as Parameters<typeof this.handleDeepseekChunk>[0]);
|
||||
}
|
||||
|
||||
// Handle text-level think tags (e.g., from nvidia/nemotron models)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ describe("chatHistoryUtils", () => {
|
|||
},
|
||||
];
|
||||
|
||||
const messages: { role: string; content: any }[] = [];
|
||||
const messages: { role: string; content: string | unknown[] }[] = [];
|
||||
addChatHistoryToMessages(rawHistory, messages);
|
||||
|
||||
expect(messages).toEqual([
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { logInfo } from "@/logger";
|
|||
|
||||
export interface ProcessedMessage {
|
||||
role: "user" | "assistant";
|
||||
content: any; // string or MessageContent[]
|
||||
content: string | unknown[]; // string or MessageContent[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -16,28 +16,29 @@ export interface ProcessedMessage {
|
|||
* @param rawHistory Array of messages from memory.loadMemoryVariables()
|
||||
* @returns Array of processed messages safe for LLM consumption
|
||||
*/
|
||||
export function processRawChatHistory(rawHistory: any[]): ProcessedMessage[] {
|
||||
export function processRawChatHistory(rawHistory: unknown[]): ProcessedMessage[] {
|
||||
const messages: ProcessedMessage[] = [];
|
||||
|
||||
for (const message of rawHistory) {
|
||||
if (!message) continue;
|
||||
const msg = message as Record<string, unknown>;
|
||||
|
||||
// Check if this is a BaseMessage with _getType method
|
||||
if (typeof message._getType === "function") {
|
||||
const messageType = message._getType();
|
||||
if (typeof msg._getType === "function") {
|
||||
const messageType = (msg._getType as () => string)();
|
||||
|
||||
// Only process human and AI messages
|
||||
if (messageType === "human") {
|
||||
messages.push({ role: "user", content: message.content });
|
||||
messages.push({ role: "user", content: msg.content as string | unknown[] });
|
||||
} else if (messageType === "ai") {
|
||||
messages.push({ role: "assistant", content: message.content });
|
||||
messages.push({ role: "assistant", content: msg.content as string | unknown[] });
|
||||
}
|
||||
// Skip system messages and unknown types
|
||||
} else if (message.content !== undefined) {
|
||||
} else if (msg.content !== undefined) {
|
||||
// Fallback for other message formats - try to infer role
|
||||
const role = inferMessageRole(message);
|
||||
const role = inferMessageRole(msg);
|
||||
if (role) {
|
||||
messages.push({ role, content: message.content });
|
||||
messages.push({ role, content: msg.content as string | unknown[] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +50,7 @@ export function processRawChatHistory(rawHistory: any[]): ProcessedMessage[] {
|
|||
* Try to infer the role from various message format properties
|
||||
* @returns 'user' | 'assistant' | null
|
||||
*/
|
||||
function inferMessageRole(message: any): "user" | "assistant" | null {
|
||||
function inferMessageRole(message: Record<string, unknown>): "user" | "assistant" | null {
|
||||
// Check various properties that might indicate the role
|
||||
if (message.role === "human" || message.role === "user" || message.sender === "user") {
|
||||
return "user";
|
||||
|
|
@ -69,8 +70,8 @@ function inferMessageRole(message: any): "user" | "assistant" | null {
|
|||
* @param messages Target messages array to add to
|
||||
*/
|
||||
export function addChatHistoryToMessages(
|
||||
rawHistory: any[],
|
||||
messages: Array<{ role: string; content: any }>
|
||||
rawHistory: unknown[],
|
||||
messages: Array<{ role: string; content: string | unknown[] }>
|
||||
): void {
|
||||
const processedHistory = processRawChatHistory(rawHistory);
|
||||
for (const msg of processedHistory) {
|
||||
|
|
@ -87,14 +88,17 @@ export interface ChatHistoryEntry {
|
|||
* Extract text content from potentially multimodal message content.
|
||||
* Replaces non-text content (images) with placeholder.
|
||||
*/
|
||||
function extractTextContent(content: any): string {
|
||||
function extractTextContent(content: string | unknown[]): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else if (Array.isArray(content)) {
|
||||
// Extract text from multimodal content, skip image_url payloads
|
||||
const textParts: string = content
|
||||
.filter((item: any) => item.type === "text")
|
||||
.map((item: any): string => (item.text as string) || "")
|
||||
.filter(
|
||||
(item): item is { type: string; text?: string } =>
|
||||
typeof item === "object" && item !== null && (item as { type?: unknown }).type === "text"
|
||||
)
|
||||
.map((item): string => item.text || "")
|
||||
.join(" ");
|
||||
return textParts || "[Image content]";
|
||||
}
|
||||
|
|
@ -229,13 +233,15 @@ export function extractConversationTurns(processedHistory: ProcessedMessage[]):
|
|||
* @returns The processed history that was added
|
||||
*/
|
||||
export async function loadAndAddChatHistory(
|
||||
memory: any,
|
||||
messages: Array<{ role: string; content: any }>
|
||||
memory: {
|
||||
loadMemoryVariables: (vars: Record<string, unknown>) => Promise<{ history?: unknown[] }>;
|
||||
},
|
||||
messages: Array<{ role: string; content: string | unknown[] }>
|
||||
): Promise<ProcessedMessage[]> {
|
||||
const memoryVariables = await memory.loadMemoryVariables({});
|
||||
const rawHistory = memoryVariables.history || [];
|
||||
|
||||
const processedHistory = rawHistory.length ? processRawChatHistory(rawHistory as any[]) : [];
|
||||
const processedHistory = rawHistory.length ? processRawChatHistory(rawHistory) : [];
|
||||
|
||||
// Add history messages directly (already compacted at save time)
|
||||
for (const msg of processedHistory) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ export interface FinishReasonResult {
|
|||
* @param chunk The streaming chunk from the LLM (AIMessageChunk)
|
||||
* @returns FinishReasonResult with truncation status and details
|
||||
*/
|
||||
export function detectTruncation(chunk: any): FinishReasonResult {
|
||||
export function detectTruncation(chunk: {
|
||||
response_metadata?: Record<string, unknown>;
|
||||
}): FinishReasonResult {
|
||||
const metadata = chunk.response_metadata || {};
|
||||
|
||||
// OpenAI, DeepSeek, Mistral, Groq use "length"
|
||||
|
|
@ -69,7 +71,10 @@ export function detectTruncation(chunk: any): FinishReasonResult {
|
|||
* @param chunk The streaming chunk from the LLM
|
||||
* @returns Token usage object or null if not available
|
||||
*/
|
||||
export function extractTokenUsage(chunk: any): {
|
||||
export function extractTokenUsage(chunk: {
|
||||
response_metadata?: Record<string, unknown>;
|
||||
usage_metadata?: { input_tokens?: number; output_tokens?: number; total_tokens?: number };
|
||||
}): {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
|
|
@ -78,31 +83,47 @@ export function extractTokenUsage(chunk: any): {
|
|||
|
||||
// OpenAI format: tokenUsage with camelCase
|
||||
if (metadata.tokenUsage) {
|
||||
const tu = metadata.tokenUsage as {
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
};
|
||||
return {
|
||||
inputTokens: metadata.tokenUsage.promptTokens,
|
||||
outputTokens: metadata.tokenUsage.completionTokens,
|
||||
totalTokens: metadata.tokenUsage.totalTokens,
|
||||
inputTokens: tu.promptTokens,
|
||||
outputTokens: tu.completionTokens,
|
||||
totalTokens: tu.totalTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// Anthropic/Bedrock/others format: usage with snake_case or camelCase
|
||||
if (metadata.usage) {
|
||||
const u = metadata.usage as {
|
||||
input_tokens?: number;
|
||||
inputTokens?: number;
|
||||
inputTokenCount?: number;
|
||||
prompt_tokens?: number;
|
||||
output_tokens?: number;
|
||||
outputTokens?: number;
|
||||
outputTokenCount?: number;
|
||||
completion_tokens?: number;
|
||||
total_tokens?: number;
|
||||
totalTokens?: number;
|
||||
};
|
||||
return {
|
||||
inputTokens:
|
||||
metadata.usage.input_tokens ||
|
||||
metadata.usage.inputTokens || // Bedrock camelCase
|
||||
metadata.usage.inputTokenCount || // Bedrock invocationMetrics
|
||||
metadata.usage.prompt_tokens,
|
||||
u.input_tokens ||
|
||||
u.inputTokens || // Bedrock camelCase
|
||||
u.inputTokenCount || // Bedrock invocationMetrics
|
||||
u.prompt_tokens,
|
||||
outputTokens:
|
||||
metadata.usage.output_tokens ||
|
||||
metadata.usage.outputTokens || // Bedrock camelCase
|
||||
metadata.usage.outputTokenCount || // Bedrock invocationMetrics
|
||||
metadata.usage.completion_tokens,
|
||||
u.output_tokens ||
|
||||
u.outputTokens || // Bedrock camelCase
|
||||
u.outputTokenCount || // Bedrock invocationMetrics
|
||||
u.completion_tokens,
|
||||
totalTokens:
|
||||
metadata.usage.total_tokens ||
|
||||
metadata.usage.totalTokens || // Bedrock camelCase
|
||||
(metadata.usage.input_tokens || metadata.usage.inputTokenCount || 0) +
|
||||
(metadata.usage.output_tokens || metadata.usage.outputTokenCount || 0),
|
||||
u.total_tokens ||
|
||||
u.totalTokens || // Bedrock camelCase
|
||||
(u.input_tokens || u.inputTokenCount || 0) + (u.output_tokens || u.outputTokenCount || 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export interface ModelAdapter {
|
|||
* @param response - The model's response text containing tool calls
|
||||
* @returns Array of parsed tool calls
|
||||
*/
|
||||
parseToolCalls?(response: string): any[];
|
||||
parseToolCalls?(response: string): unknown[];
|
||||
|
||||
/**
|
||||
* Check if model needs special handling
|
||||
|
|
@ -288,7 +288,7 @@ class GPTModelAdapter extends BaseModelAdapter {
|
|||
* Check if this is a GPT-5 model
|
||||
* @returns True if the model is in the GPT-5 family
|
||||
*/
|
||||
protected isGPT5Model(): boolean {
|
||||
isGPT5Model(): boolean {
|
||||
return this.modelName.includes("gpt-5") || this.modelName.includes("gpt5");
|
||||
}
|
||||
buildSystemPromptSections(
|
||||
|
|
@ -675,8 +675,8 @@ REMEMBER: It is better to say "I only searched your notes, not the web" than to
|
|||
export class ModelAdapterFactory {
|
||||
static createAdapter(model: BaseChatModel): ModelAdapter {
|
||||
const modelName: string = (
|
||||
((model as any).modelName as string) ||
|
||||
((model as any).model as string) ||
|
||||
(model as { modelName?: string }).modelName ||
|
||||
(model as { model?: string }).model ||
|
||||
""
|
||||
).toLowerCase();
|
||||
|
||||
|
|
@ -686,7 +686,7 @@ export class ModelAdapterFactory {
|
|||
if (modelName.includes("gpt")) {
|
||||
const adapter = new GPTModelAdapter(modelName);
|
||||
// Log if it's a GPT-5 model for debugging
|
||||
if ((adapter as any).isGPT5Model()) {
|
||||
if (adapter.isGPT5Model()) {
|
||||
logInfo("Using GPTModelAdapter with GPT-5 specific enhancements");
|
||||
} else {
|
||||
logInfo("Using GPTModelAdapter");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const createAdapter = () => ({
|
|||
basePrompt: string,
|
||||
toolDescriptions: string,
|
||||
toolNames?: string[],
|
||||
toolMetadata?: any[]
|
||||
toolMetadata?: unknown[]
|
||||
): PromptSection[] => [
|
||||
{
|
||||
id: "system",
|
||||
|
|
|
|||
|
|
@ -85,14 +85,14 @@ function buildLayeredViewFromMessages(
|
|||
};
|
||||
|
||||
// Helper to extract text content from message
|
||||
const getTextContent = (msg: any): string => {
|
||||
const getTextContent = (msg: { role?: unknown; content?: unknown }): string => {
|
||||
if (typeof msg.content === "string") {
|
||||
return msg.content as string;
|
||||
return msg.content;
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
const textParts: string[] = msg.content
|
||||
.filter((item: any) => item.type === "text")
|
||||
.map((item: any): string => item.text as string);
|
||||
const textParts: string[] = (msg.content as Array<{ type?: string; text?: string }>)
|
||||
.filter((item) => item.type === "text")
|
||||
.map((item): string => item.text ?? "");
|
||||
return textParts.join("\n");
|
||||
}
|
||||
return "";
|
||||
|
|
@ -103,7 +103,7 @@ function buildLayeredViewFromMessages(
|
|||
let historyCount = 0;
|
||||
|
||||
for (let i = 0; i < messageArray.length; i++) {
|
||||
const msg: any = messageArray[i];
|
||||
const msg = messageArray[i] as { role?: unknown; content?: unknown };
|
||||
const content = getTextContent(msg);
|
||||
|
||||
if (msg.role === "system") {
|
||||
|
|
@ -194,7 +194,9 @@ function buildLayeredViewFromMessages(
|
|||
}
|
||||
|
||||
// Last user message
|
||||
const lastMsg: any = messageArray[messageArray.length - 1];
|
||||
const lastMsg = messageArray[messageArray.length - 1] as
|
||||
| { role?: unknown; content?: unknown }
|
||||
| undefined;
|
||||
if (lastMsg && lastMsg.role === "user") {
|
||||
lines.push("━━━ USER MESSAGE ━━━");
|
||||
lines.push("");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,27 @@
|
|||
import { logInfo, logWarn, logMarkdownBlock, logTable } from "@/logger";
|
||||
import { sanitizeContentForCitations } from "@/LLMProviders/chainRunner/utils/citationUtils";
|
||||
|
||||
/**
|
||||
* Raw document shape returned by search/retrieval tools.
|
||||
* Fields are optional because different providers may omit some.
|
||||
*/
|
||||
export interface SearchDoc {
|
||||
title?: string;
|
||||
path?: string;
|
||||
content?: string;
|
||||
mtime?: string | number;
|
||||
rerank_score?: number;
|
||||
score?: number;
|
||||
explanation?: unknown;
|
||||
includeInContext?: boolean;
|
||||
__sourceId?: string | number;
|
||||
collection_name?: string;
|
||||
source_id?: string | number;
|
||||
matchType?: string;
|
||||
source?: string;
|
||||
chunkId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality summary for search results.
|
||||
* Helps the LLM evaluate whether results are adequate or if re-search is needed.
|
||||
|
|
@ -20,7 +41,7 @@ export interface QualitySummary {
|
|||
* @param searchResults - Array of search results with scores
|
||||
* @returns Quality summary with counts by relevance tier
|
||||
*/
|
||||
export function generateQualitySummary(searchResults: any[]): QualitySummary {
|
||||
export function generateQualitySummary(searchResults: SearchDoc[]): QualitySummary {
|
||||
if (!Array.isArray(searchResults) || searchResults.length === 0) {
|
||||
return { high: 0, medium: 0, low: 0, total: 0, averageScore: 0 };
|
||||
}
|
||||
|
|
@ -83,7 +104,9 @@ export function formatSearchResultsForLLM(searchResults: unknown): string {
|
|||
}
|
||||
|
||||
// Filter documents that should be included in context
|
||||
const includedDocs = searchResults.filter((doc) => doc.includeInContext !== false);
|
||||
const includedDocs = (searchResults as SearchDoc[]).filter(
|
||||
(doc) => doc.includeInContext !== false
|
||||
);
|
||||
|
||||
if (includedDocs.length === 0) {
|
||||
return "No relevant documents found.";
|
||||
|
|
@ -91,7 +114,7 @@ export function formatSearchResultsForLLM(searchResults: unknown): string {
|
|||
|
||||
// Format each document with essential metadata
|
||||
const formattedDocs = includedDocs
|
||||
.map((doc: any, idx: number) => {
|
||||
.map((doc, idx: number) => {
|
||||
const title = doc.title || "Untitled";
|
||||
const path = doc.path || "";
|
||||
// Optional stable source id if provided by caller; fallback to order
|
||||
|
|
@ -100,7 +123,7 @@ export function formatSearchResultsForLLM(searchResults: unknown): string {
|
|||
// Safely handle mtime - check validity before converting
|
||||
let modified: string | null = null;
|
||||
if (doc.mtime) {
|
||||
const date = new Date(doc.mtime as string | number | Date);
|
||||
const date = new Date(doc.mtime);
|
||||
if (!isNaN(date.getTime())) {
|
||||
modified = date.toISOString();
|
||||
}
|
||||
|
|
@ -156,12 +179,12 @@ export function formatSearchResultStringForLLM(resultString: string): string {
|
|||
*/
|
||||
export function extractSourcesFromSearchResults(
|
||||
searchResults: unknown
|
||||
): { title: string; path: string; score: number; explanation?: any }[] {
|
||||
): { title: string; path: string; score: number; explanation?: unknown }[] {
|
||||
if (!Array.isArray(searchResults)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return searchResults.map((doc: any) => ({
|
||||
return (searchResults as SearchDoc[]).map((doc) => ({
|
||||
title: doc.title || doc.path || "Untitled",
|
||||
path: doc.path || doc.title || "",
|
||||
score: doc.rerank_score || doc.score || 0,
|
||||
|
|
@ -189,19 +212,20 @@ function toIsoString(ts: unknown): string {
|
|||
* Create a concise, single-line summary of an explanation object.
|
||||
* Includes lexical matches, semantic score, folder/graph boosts, and score adjustments.
|
||||
*/
|
||||
export function summarizeExplanation(explanation: any): string {
|
||||
export function summarizeExplanation(explanation: unknown): string {
|
||||
if (!explanation) return "";
|
||||
|
||||
const parts: string[] = [];
|
||||
const exp = explanation as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
// Lexical matches summary
|
||||
if (Array.isArray(explanation.lexicalMatches) && explanation.lexicalMatches.length > 0) {
|
||||
if (Array.isArray(exp.lexicalMatches) && exp.lexicalMatches.length > 0) {
|
||||
const fields = new Set<string>();
|
||||
const terms = new Set<string>();
|
||||
for (const m of explanation.lexicalMatches) {
|
||||
if (m?.field) fields.add(String(m.field));
|
||||
if (m?.query) terms.add(String(m.query));
|
||||
for (const m of exp.lexicalMatches as Array<{ field?: unknown; query?: unknown }>) {
|
||||
if (typeof m?.field === "string") fields.add(m.field);
|
||||
if (typeof m?.query === "string") terms.add(m.query);
|
||||
}
|
||||
const fieldsStr = Array.from(fields).join("/");
|
||||
const termsStr = Array.from(terms).slice(0, 3).join(", ");
|
||||
|
|
@ -209,24 +233,32 @@ export function summarizeExplanation(explanation: any): string {
|
|||
}
|
||||
|
||||
// Semantic score
|
||||
if (typeof explanation.semanticScore === "number" && explanation.semanticScore > 0) {
|
||||
parts.push(`Semantic: ${(explanation.semanticScore * 100).toFixed(1)}%`);
|
||||
if (typeof exp.semanticScore === "number" && exp.semanticScore > 0) {
|
||||
parts.push(`Semantic: ${(exp.semanticScore * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// Folder boost
|
||||
if (explanation.folderBoost && typeof explanation.folderBoost.boostFactor === "number") {
|
||||
const fb = explanation.folderBoost;
|
||||
if (
|
||||
exp.folderBoost &&
|
||||
typeof (exp.folderBoost as { boostFactor?: number }).boostFactor === "number"
|
||||
) {
|
||||
const fb = exp.folderBoost as { boostFactor: number; folder?: string };
|
||||
const folder = fb.folder || "root";
|
||||
parts.push(`Folder +${fb.boostFactor.toFixed(2)} (${folder})`);
|
||||
}
|
||||
|
||||
// Graph connections (query-aware boost)
|
||||
if (explanation.graphConnections && typeof explanation.graphConnections === "object") {
|
||||
const gc = explanation.graphConnections;
|
||||
if (exp.graphConnections && typeof exp.graphConnections === "object") {
|
||||
const gc = exp.graphConnections as {
|
||||
backlinks?: number;
|
||||
coCitations?: number;
|
||||
sharedTags?: number;
|
||||
score?: number;
|
||||
};
|
||||
const bits: string[] = [];
|
||||
if (gc.backlinks > 0) bits.push(`${gc.backlinks} backlinks`);
|
||||
if (gc.coCitations > 0) bits.push(`${gc.coCitations} co-cites`);
|
||||
if (gc.sharedTags > 0) bits.push(`${gc.sharedTags} tags`);
|
||||
if ((gc.backlinks ?? 0) > 0) bits.push(`${gc.backlinks} backlinks`);
|
||||
if ((gc.coCitations ?? 0) > 0) bits.push(`${gc.coCitations} co-cites`);
|
||||
if ((gc.sharedTags ?? 0) > 0) bits.push(`${gc.sharedTags} tags`);
|
||||
if (typeof gc.score === "number") {
|
||||
parts.push(`Graph ${gc.score.toFixed(1)}${bits.length ? ` (${bits.join(", ")})` : ""}`);
|
||||
} else if (bits.length) {
|
||||
|
|
@ -236,21 +268,21 @@ export function summarizeExplanation(explanation: any): string {
|
|||
|
||||
// Legacy graph boost
|
||||
if (
|
||||
explanation.graphBoost &&
|
||||
typeof explanation.graphBoost.boostFactor === "number" &&
|
||||
!explanation.graphConnections
|
||||
exp.graphBoost &&
|
||||
typeof (exp.graphBoost as { boostFactor?: number }).boostFactor === "number" &&
|
||||
!exp.graphConnections
|
||||
) {
|
||||
const gb = explanation.graphBoost;
|
||||
const gb = exp.graphBoost as { boostFactor: number; connections?: number };
|
||||
parts.push(`Graph +${gb.boostFactor.toFixed(2)} (${gb.connections} connections)`);
|
||||
}
|
||||
|
||||
// Score adjustment
|
||||
if (
|
||||
typeof explanation.baseScore === "number" &&
|
||||
typeof explanation.finalScore === "number" &&
|
||||
explanation.baseScore !== explanation.finalScore
|
||||
typeof exp.baseScore === "number" &&
|
||||
typeof exp.finalScore === "number" &&
|
||||
exp.baseScore !== exp.finalScore
|
||||
) {
|
||||
parts.push(`Score: ${explanation.baseScore.toFixed(4)}→${explanation.finalScore.toFixed(4)}`);
|
||||
parts.push(`Score: ${exp.baseScore.toFixed(4)}→${exp.finalScore.toFixed(4)}`);
|
||||
}
|
||||
} catch {
|
||||
// Ignore explanation parsing errors, leave parts as-is
|
||||
|
|
@ -278,8 +310,8 @@ export function summarizeExplanation(explanation: any): string {
|
|||
* @returns Formatted XML string with `<filterResults>` and `<searchResults>` sections
|
||||
*/
|
||||
export function formatSplitSearchResultsForLLM(
|
||||
filterDocs: any[],
|
||||
searchDocs: any[],
|
||||
filterDocs: SearchDoc[],
|
||||
searchDocs: SearchDoc[],
|
||||
startId = 1
|
||||
): string {
|
||||
let currentId = startId;
|
||||
|
|
@ -288,7 +320,7 @@ export function formatSplitSearchResultsForLLM(
|
|||
// Format filter results
|
||||
if (filterDocs.length > 0) {
|
||||
const filterXml = filterDocs
|
||||
.map((doc: any) => {
|
||||
.map((doc) => {
|
||||
const id = doc.__sourceId || currentId++;
|
||||
const title = doc.title || "Untitled";
|
||||
const path = doc.path || "";
|
||||
|
|
@ -313,7 +345,7 @@ ${doc.content || ""}
|
|||
// Format search results
|
||||
if (searchDocs.length > 0) {
|
||||
const searchXml = searchDocs
|
||||
.map((doc: any) => {
|
||||
.map((doc) => {
|
||||
const id = doc.__sourceId || currentId++;
|
||||
const title = doc.title || "Untitled";
|
||||
const path = doc.path || "";
|
||||
|
|
@ -394,12 +426,12 @@ export function formatMetadataOnlyDocuments(docs: unknown, snippetLength = 300):
|
|||
return "";
|
||||
}
|
||||
|
||||
const fileElements = docs
|
||||
.map((doc: any) => {
|
||||
const fileElements = (docs as SearchDoc[])
|
||||
.map((doc) => {
|
||||
const title = doc.title || "Untitled";
|
||||
const path = doc.path || "";
|
||||
const modified = toIsoString(doc.mtime);
|
||||
const content = sanitizeContentForCitations((doc.content as string) || "");
|
||||
const content = sanitizeContentForCitations(doc.content || "");
|
||||
const snippet = content.slice(0, snippetLength);
|
||||
|
||||
const pathEl = path ? `\n<path>${path}</path>` : "";
|
||||
|
|
@ -413,7 +445,7 @@ export function formatMetadataOnlyDocuments(docs: unknown, snippetLength = 300):
|
|||
return `<additionalMatches count="${docs.length}" note="These results contain titles and metadata only. To read the full content of a note, call the readNote tool with its path.">\n${fileElements}\n</additionalMatches>`;
|
||||
}
|
||||
|
||||
export function logSearchResultsDebugTable(searchResults: any[]): void {
|
||||
export function logSearchResultsDebugTable(searchResults: SearchDoc[]): void {
|
||||
if (!Array.isArray(searchResults) || searchResults.length === 0) {
|
||||
logInfo("Search Results: (none)");
|
||||
return;
|
||||
|
|
@ -428,7 +460,7 @@ export function logSearchResultsDebugTable(searchResults: any[]): void {
|
|||
};
|
||||
|
||||
let includedCount = 0;
|
||||
const rows: Row[] = searchResults.map((doc: any) => {
|
||||
const rows: Row[] = searchResults.map((doc) => {
|
||||
const mtime = toIsoString(doc.mtime);
|
||||
const scoreNum = typeof doc.rerank_score === "number" ? doc.rerank_score : doc.score || 0;
|
||||
const score = (Number.isFinite(scoreNum) ? scoreNum : 0).toFixed(4);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { StructuredTool } from "@langchain/core/tools";
|
||||
import { logError, logInfo, logWarn } from "@/logger";
|
||||
import { checkIsPlusUser, isSelfHostModeValid } from "@/plusUtils";
|
||||
import { getSettings } from "@/settings/model";
|
||||
|
|
@ -30,7 +31,7 @@ export interface ToolExecutionResult {
|
|||
*/
|
||||
export async function executeSequentialToolCall(
|
||||
toolCall: ToolCall,
|
||||
availableTools: any[],
|
||||
availableTools: Pick<StructuredTool, "name" | "invoke">[],
|
||||
originalUserMessage?: string
|
||||
): Promise<ToolExecutionResult> {
|
||||
const DEFAULT_TOOL_TIMEOUT = 120000; // 120 seconds timeout per tool
|
||||
|
|
@ -49,7 +50,7 @@ export async function executeSequentialToolCall(
|
|||
const tool = availableTools.find((t) => t.name === toolCall.name);
|
||||
|
||||
if (!tool) {
|
||||
const availableToolNames = availableTools.map((t): string => t.name as string).join(", ");
|
||||
const availableToolNames = availableTools.map((t): string => t.name).join(", ");
|
||||
return {
|
||||
toolName: toolCall.name,
|
||||
result: `Error: Tool '${toolCall.name}' not found. Available tools: ${availableToolNames}. Make sure you have the tool enabled in the Agent settings.`,
|
||||
|
|
@ -213,7 +214,10 @@ export function getToolEmoji(toolName: string): string {
|
|||
/**
|
||||
* Get user confirmation message for tool call
|
||||
*/
|
||||
export function getToolConfirmtionMessage(toolName: string, toolArgs?: any): string | null {
|
||||
export function getToolConfirmtionMessage(
|
||||
toolName: string,
|
||||
toolArgs?: Record<string, unknown>
|
||||
): string | null {
|
||||
if (toolName == "writeFile" || toolName == "editFile") {
|
||||
return "Accept / reject in the Preview";
|
||||
}
|
||||
|
|
@ -283,11 +287,11 @@ export function logToolResult(toolName: string, result: ToolExecutionResult): vo
|
|||
* If path is not available, falls back to title
|
||||
*/
|
||||
export function deduplicateSources(
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[]
|
||||
): { title: string; path: string; score: number; explanation?: any }[] {
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[]
|
||||
): { title: string; path: string; score: number; explanation?: unknown }[] {
|
||||
const uniqueSources = new Map<
|
||||
string,
|
||||
{ title: string; path: string; score: number; explanation?: any }
|
||||
{ title: string; path: string; score: number; explanation?: unknown }
|
||||
>();
|
||||
|
||||
for (const source of sources) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { processRawChatHistory, processedMessagesToTextOnly } from "./chatHistor
|
|||
*/
|
||||
export interface BuildPromptDebugSectionsOptions {
|
||||
systemSections: PromptSection[];
|
||||
rawHistory?: any[];
|
||||
rawHistory?: unknown[];
|
||||
adapterName: string;
|
||||
originalUserMessage: string;
|
||||
enhancedUserMessage: string;
|
||||
|
|
|
|||
|
|
@ -38,35 +38,29 @@ import { ChatXAI } from "@langchain/xai";
|
|||
import { MissingApiKeyError, MissingPlusLicenseError } from "@/error";
|
||||
import { Notice } from "obsidian";
|
||||
import { ChatOpenRouter } from "./ChatOpenRouter";
|
||||
import { ChatLMStudio, type ChatLMStudioInput } from "./ChatLMStudio";
|
||||
import { ChatLMStudio } from "./ChatLMStudio";
|
||||
import { BedrockChatModel, type BedrockChatModelFields } from "./BedrockChatModel";
|
||||
import { GitHubCopilotChatModel } from "@/LLMProviders/githubCopilot/GitHubCopilotChatModel";
|
||||
import {
|
||||
GitHubCopilotResponsesModel,
|
||||
type GitHubCopilotResponsesModelParams,
|
||||
} from "@/LLMProviders/githubCopilot/GitHubCopilotResponsesModel";
|
||||
import { GitHubCopilotResponsesModel } from "@/LLMProviders/githubCopilot/GitHubCopilotResponsesModel";
|
||||
|
||||
// Patch BaseLanguageModel.prototype.getNumTokens once at module load to prevent
|
||||
// tiktoken CDN fetches. LangChain's default getNumTokens() downloads a ~3MB BPE
|
||||
// vocabulary from tiktoken.pages.dev, which blocks all LLM calls when the CDN is
|
||||
// unreachable. This char/4 estimation is the same fallback LangChain uses internally
|
||||
// before tiktoken loads. Actual token usage comes from API response metadata.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching a private prototype method requires any cast
|
||||
(BaseLanguageModel.prototype as any).getNumTokens = async (
|
||||
content: string | Array<{ type: string; text?: string }>
|
||||
) => {
|
||||
const text =
|
||||
typeof content === "string"
|
||||
? content
|
||||
: content
|
||||
.map((item: any): string =>
|
||||
typeof item === "string" ? item : ((item.text as string) ?? "")
|
||||
)
|
||||
.join("");
|
||||
: content.map((item: { type: string; text?: string }): string => item.text ?? "").join("");
|
||||
return Math.ceil(text.length / 4);
|
||||
};
|
||||
|
||||
type ChatConstructorType = {
|
||||
new (config: any): any;
|
||||
new (config: Record<string, unknown>): BaseChatModel;
|
||||
};
|
||||
|
||||
const CHAT_PROVIDER_CONSTRUCTORS = {
|
||||
|
|
@ -253,7 +247,7 @@ export default class ChatModelManager {
|
|||
},
|
||||
}),
|
||||
},
|
||||
[ChatModelProviders.AZURE_OPENAI]: await (async (): Promise<any> => {
|
||||
[ChatModelProviders.AZURE_OPENAI]: await (async (): Promise<Record<string, unknown>> => {
|
||||
const azureUrl = normalizeAzureUrl(customModel.baseUrl);
|
||||
return {
|
||||
modelName: customModel.baseUrl
|
||||
|
|
@ -495,7 +489,7 @@ export default class ChatModelManager {
|
|||
maxTokens: number,
|
||||
_temperature: number | undefined,
|
||||
customModel?: CustomModel
|
||||
): any {
|
||||
): Record<string, unknown> {
|
||||
const settings = getSettings();
|
||||
const modelInfo = getModelInfo(modelName);
|
||||
const resolvedTemperature = this.getTemperatureForModel(
|
||||
|
|
@ -504,7 +498,7 @@ export default class ChatModelManager {
|
|||
settings
|
||||
);
|
||||
|
||||
const config: any = {
|
||||
const config: Record<string, unknown> = {
|
||||
maxTokens,
|
||||
temperature: resolvedTemperature,
|
||||
};
|
||||
|
|
@ -597,7 +591,7 @@ export default class ChatModelManager {
|
|||
* This prevents passing undefined values to providers that don't support them
|
||||
*/
|
||||
private getProviderSpecificParams(provider: ChatModelProviders, customModel: CustomModel) {
|
||||
const params: Record<string, any> = {};
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
// Add topP only if defined
|
||||
if (customModel.topP !== undefined) {
|
||||
|
|
@ -693,8 +687,9 @@ export default class ChatModelManager {
|
|||
}
|
||||
|
||||
getProviderConstructor(model: CustomModel): ChatConstructorType {
|
||||
const constructor: ChatConstructorType =
|
||||
CHAT_PROVIDER_CONSTRUCTORS[model.provider as ChatModelProviders];
|
||||
const constructor: ChatConstructorType = CHAT_PROVIDER_CONSTRUCTORS[
|
||||
model.provider as ChatModelProviders
|
||||
] as unknown as ChatConstructorType;
|
||||
if (!constructor) {
|
||||
console.warn(`Unknown provider: ${model.provider} for model: ${model.name}`);
|
||||
throw new Error(`Unknown provider: ${model.provider} for model: ${model.name}`);
|
||||
|
|
@ -828,7 +823,7 @@ export default class ChatModelManager {
|
|||
const modelInfo = getModelInfo(model.name);
|
||||
|
||||
// For GPT-5 models, automatically use Responses API for proper verbosity support
|
||||
const constructorConfig: any = { ...modelConfig };
|
||||
const constructorConfig: Record<string, unknown> = { ...modelConfig };
|
||||
const useCopilotResponses = shouldUseGitHubCopilotResponsesApi(model);
|
||||
if (
|
||||
modelInfo.isGPT5 &&
|
||||
|
|
@ -850,20 +845,18 @@ export default class ChatModelManager {
|
|||
(model.provider as ChatModelProviders) === ChatModelProviders.LM_STUDIO &&
|
||||
model.useResponsesApi !== false
|
||||
) {
|
||||
const lmStudioInstance = new ChatLMStudio(constructorConfig as ChatLMStudioInput);
|
||||
const lmStudioInstance = new ChatLMStudio(constructorConfig);
|
||||
logInfo(`[ChatModelManager] Using Responses API for LM Studio model: ${model.name}`);
|
||||
return lmStudioInstance;
|
||||
}
|
||||
|
||||
if (useCopilotResponses) {
|
||||
return new GitHubCopilotResponsesModel(
|
||||
constructorConfig as GitHubCopilotResponsesModelParams
|
||||
);
|
||||
return new GitHubCopilotResponsesModel(constructorConfig);
|
||||
}
|
||||
|
||||
const newModelInstance = new selectedModel.AIConstructor(constructorConfig);
|
||||
|
||||
return newModelInstance as BaseChatModel;
|
||||
return newModelInstance;
|
||||
}
|
||||
|
||||
validateChatModel(chatModel: BaseChatModel): boolean {
|
||||
|
|
@ -917,7 +910,7 @@ export default class ChatModelManager {
|
|||
const pingMaxTokens = modelInfo.isThinkingEnabled ? 4096 : 30;
|
||||
const tokenConfig = { maxTokens: pingMaxTokens };
|
||||
|
||||
const constructorConfig: any = {
|
||||
const constructorConfig: Record<string, unknown> = {
|
||||
...pingConfig,
|
||||
...tokenConfig,
|
||||
};
|
||||
|
|
@ -940,11 +933,9 @@ export default class ChatModelManager {
|
|||
const testModel =
|
||||
(model.provider as ChatModelProviders) === ChatModelProviders.LM_STUDIO &&
|
||||
model.useResponsesApi !== false
|
||||
? new ChatLMStudio(constructorConfig as ChatLMStudioInput)
|
||||
? new ChatLMStudio(constructorConfig)
|
||||
: useCopilotResponses
|
||||
? new GitHubCopilotResponsesModel(
|
||||
constructorConfig as GitHubCopilotResponsesModelParams
|
||||
)
|
||||
? new GitHubCopilotResponsesModel(constructorConfig)
|
||||
: new (this.getProviderConstructor(modelToTest))(constructorConfig);
|
||||
await testModel.invoke([{ role: "user", content: "hello" }], {
|
||||
timeout: 8000,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { BrevilabsClient } from "./brevilabsClient";
|
|||
import { CustomJinaEmbeddings } from "./CustomJinaEmbeddings";
|
||||
import { CustomOpenAIEmbeddings } from "./CustomOpenAIEmbeddings";
|
||||
|
||||
type EmbeddingConstructorType = new (config: any) => Embeddings;
|
||||
type EmbeddingConstructorType = new (config: Record<string, unknown>) => Embeddings;
|
||||
|
||||
const EMBEDDING_PROVIDER_CONSTRUCTORS = {
|
||||
[EmbeddingModelProviders.COPILOT_PLUS]: CustomOpenAIEmbeddings,
|
||||
|
|
@ -180,7 +180,7 @@ export default class EmbeddingManager {
|
|||
}
|
||||
}
|
||||
|
||||
private async getEmbeddingConfig(customModel: CustomModel): Promise<any> {
|
||||
private async getEmbeddingConfig(customModel: CustomModel): Promise<Record<string, unknown>> {
|
||||
const settings = getSettings();
|
||||
const modelName = customModel.name;
|
||||
|
||||
|
|
|
|||
|
|
@ -148,11 +148,11 @@ export class GitHubCopilotChatModel extends ChatOpenAICompletions {
|
|||
* @returns A normalized LangChain message chunk.
|
||||
*/
|
||||
protected override _convertCompletionsDeltaToBaseMessageChunk(
|
||||
delta: Record<string, any>,
|
||||
rawResponse: any,
|
||||
delta: Record<string, unknown>,
|
||||
rawResponse: unknown,
|
||||
// Reason: Parent expects OpenAI's ChatCompletionRole type, but we accept any string
|
||||
// to avoid coupling to the exact OpenAI SDK type. Cast is safe because we pass through.
|
||||
defaultRole?: any
|
||||
defaultRole?: string
|
||||
): BaseMessageChunk {
|
||||
// Reason: Copilot API omits delta.role when proxying Claude models.
|
||||
// The parent converter uses `delta.role ?? defaultRole` to determine message type.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default class MemoryManager {
|
|||
await this.memory.clear();
|
||||
}
|
||||
|
||||
async loadMemoryVariables(): Promise<any> {
|
||||
async loadMemoryVariables(): Promise<Record<string, unknown>> {
|
||||
const variables = await this.memory.loadMemoryVariables({});
|
||||
if (this.debug) logInfo("Loaded memory variables:", variables);
|
||||
return variables;
|
||||
|
|
@ -59,7 +59,10 @@ export default class MemoryManager {
|
|||
* The output (assistant response) is compacted to reduce memory bloat from
|
||||
* accumulated tool results (localSearch, readNote, etc.).
|
||||
*/
|
||||
async saveContext(input: any, output: any): Promise<void> {
|
||||
async saveContext(
|
||||
input: Record<string, unknown>,
|
||||
output: Record<string, unknown> | string
|
||||
): Promise<void> {
|
||||
// Compact the output to prevent memory bloat from tool results
|
||||
const compactedOutput =
|
||||
typeof output === "string"
|
||||
|
|
@ -73,7 +76,7 @@ export default class MemoryManager {
|
|||
logInfo("Saving to memory - Input:", input, "Output (compacted):", compactedOutput);
|
||||
}
|
||||
await this.memory.saveContext(
|
||||
input as Parameters<typeof this.memory.saveContext>[0],
|
||||
input,
|
||||
compactedOutput as Parameters<typeof this.memory.saveContext>[1]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { hasSelfHostSearchKey, selfHostWebSearch } from "./selfHostServices";
|
|||
|
||||
const mockGetSettings = jest.fn();
|
||||
jest.mock("@/settings/model", () => ({
|
||||
getSettings: (): any => mockGetSettings(),
|
||||
getSettings: (): unknown => mockGetSettings(),
|
||||
}));
|
||||
|
||||
jest.mock("@/encryptionService", () => ({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ModelCapability, ReasoningEffort, Verbosity } from "@/constants";
|
|||
import { settingsAtom, settingsStore } from "@/settings/model";
|
||||
import { SelectedTextContext } from "@/types/message";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
const userModelKeyAtom = atom<string | null>(null);
|
||||
const modelKeyAtom = atom(
|
||||
|
|
@ -128,7 +129,7 @@ export interface ModelConfig {
|
|||
export interface SetChainOptions {
|
||||
prompt?: ChatPromptTemplate;
|
||||
chatModel?: BaseChatModel;
|
||||
noteFile?: any;
|
||||
noteFile?: TFile;
|
||||
abortController?: AbortController;
|
||||
refreshIndex?: boolean;
|
||||
}
|
||||
|
|
|
|||
2
src/cache/fileCache.ts
vendored
2
src/cache/fileCache.ts
vendored
|
|
@ -8,7 +8,7 @@ export interface FileCacheEntry<T> {
|
|||
}
|
||||
|
||||
export class FileCache<T> {
|
||||
private static instance: FileCache<any>;
|
||||
private static instance: FileCache<unknown>;
|
||||
private cacheDir: string;
|
||||
private memoryCache: Map<string, FileCacheEntry<T>> = new Map();
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface ConversationalRetrievalChainParams {
|
|||
};
|
||||
}
|
||||
|
||||
export interface Document<T = Record<string, any>> {
|
||||
export interface Document<T = Record<string, unknown>> {
|
||||
// Structure of Document, possibly including pageContent, metadata, etc.
|
||||
pageContent: string;
|
||||
metadata: T;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function CustomCommandSettingsModalContent({
|
|||
const [command, setCommand] = useState(initialCommand);
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
|
||||
const handleUpdate = (field: keyof CustomCommand, value: any) => {
|
||||
const handleUpdate = (field: keyof CustomCommand, value: CustomCommand[keyof CustomCommand]) => {
|
||||
setCommand((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
|
|
|
|||
|
|
@ -428,28 +428,30 @@ export function registerCommands(
|
|||
}
|
||||
|
||||
// Map hits to chunks (getDocsByPath returns {document, score} format)
|
||||
const chunks: any[] = hits.map((hit: { document: any }): any => hit.document);
|
||||
const chunks: Record<string, unknown>[] = hits.map(
|
||||
(hit) => hit.document as unknown as Record<string, unknown>
|
||||
);
|
||||
const content = [
|
||||
`# Embedding Debug: ${activeFile.basename}`,
|
||||
"",
|
||||
`**Path:** ${activeFile.path}`,
|
||||
`**Chunks:** ${chunks.length}`,
|
||||
`**Embedding Model:** ${chunks[0]?.embeddingModel || "unknown"}`,
|
||||
`**Embedding Model:** ${(chunks[0]?.embeddingModel as string | undefined) || "unknown"}`,
|
||||
"",
|
||||
...chunks.flatMap((chunk: any, index: number) => {
|
||||
const embedding = chunk.embedding || [];
|
||||
...chunks.flatMap((chunk: Record<string, unknown>, index: number) => {
|
||||
const embedding = (chunk.embedding as number[] | undefined) || [];
|
||||
const preview = embedding
|
||||
.slice(0, 10)
|
||||
.map((v: number) => v.toFixed(6))
|
||||
.join(", ");
|
||||
return [
|
||||
`## Chunk ${index + 1}`,
|
||||
`- **ID:** ${chunk.id}`,
|
||||
`- **Content Preview:** "${(chunk.content || "").substring(0, 200)}..."`,
|
||||
`- **ID:** ${chunk.id as string}`,
|
||||
`- **Content Preview:** "${((chunk.content as string | undefined) || "").substring(0, 200)}..."`,
|
||||
`- **Vector Length:** ${embedding.length}`,
|
||||
`- **Vector Preview:** [${preview}${embedding.length > 10 ? ", ..." : ""}]`,
|
||||
`- **Tags:** ${(chunk.tags || []).join(", ") || "none"}`,
|
||||
`- **Characters:** ${chunk.nchars || 0}`,
|
||||
`- **Tags:** ${((chunk.tags as string[] | undefined) || []).join(", ") || "none"}`,
|
||||
`- **Characters:** ${(chunk.nchars as number | undefined) || 0}`,
|
||||
"",
|
||||
];
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -278,7 +278,10 @@ const ChatInternal: React.FC<ChatProps & { chatInput: ReturnType<typeof useChatI
|
|||
|
||||
try {
|
||||
// Create message content array
|
||||
const content: any[] = [];
|
||||
type MessageContentItem =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image_url"; image_url: { url: string } };
|
||||
const content: MessageContentItem[] = [];
|
||||
|
||||
// Add text content if present
|
||||
if (inputMessage) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import { TypeaheadMenuPopover } from "./TypeaheadMenuPopover";
|
||||
import {
|
||||
useAtMentionCategories,
|
||||
|
|
@ -8,11 +8,12 @@ import {
|
|||
CategoryOption,
|
||||
} from "./hooks/useAtMentionCategories";
|
||||
import { useAtMentionSearch } from "./hooks/useAtMentionSearch";
|
||||
import type { WebTabContext } from "@/types/message";
|
||||
|
||||
interface AtMentionTypeaheadProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (category: AtMentionCategory, data: any) => void;
|
||||
onSelect: (category: AtMentionCategory, data: TFile | string | TFolder | WebTabContext) => void;
|
||||
isCopilotPlus?: boolean;
|
||||
currentActiveFile?: TFile | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AlertCircle, CheckCircle, CircleDashed, FileText, Loader2, X } from "lucide-react";
|
||||
import { Platform, TFile } from "obsidian";
|
||||
import { Platform, TFile, TFolder } from "obsidian";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -32,10 +32,10 @@ interface ChatContextMenuProps {
|
|||
contextFolders: string[];
|
||||
contextWebTabs: WebTabContext[];
|
||||
selectedTextContexts?: SelectedTextContext[];
|
||||
onRemoveContext: (category: string, data: any) => void;
|
||||
onRemoveContext: (category: string, data: string) => void;
|
||||
showProgressCard: () => void;
|
||||
showIndexingCard?: () => void;
|
||||
onTypeaheadSelect: (category: string, data: any) => void;
|
||||
onTypeaheadSelect: (category: string, data: TFile | string | TFolder | WebTabContext) => void;
|
||||
lexicalEditorRef?: React.RefObject<{ focus: () => void }>;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ function ContextSelection({
|
|||
onRemoveContext,
|
||||
}: {
|
||||
selectedText: SelectedTextContext;
|
||||
onRemoveContext: (category: string, data: any) => void;
|
||||
onRemoveContext: (category: string, data: string) => void;
|
||||
}) {
|
||||
// Handle web selected text
|
||||
if (isWebSelectedTextContext(selectedText)) {
|
||||
|
|
@ -123,7 +123,10 @@ export const ChatContextMenu: React.FC<ChatContextMenuProps> = ({
|
|||
};
|
||||
|
||||
// Simple wrapper that adds focus management to the ContextControl handler
|
||||
const handleTypeaheadSelect = (category: string, data: any) => {
|
||||
const handleTypeaheadSelect = (
|
||||
category: string,
|
||||
data: TFile | string | TFolder | WebTabContext
|
||||
) => {
|
||||
// Delegate to ContextControl handler
|
||||
onTypeaheadSelect(category, data);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ import {
|
|||
import { TokenCounter } from "./TokenCounter";
|
||||
import { ChatSettingsPopover } from "@/components/chat-components/ChatSettingsPopover";
|
||||
|
||||
/** Minimal type for the internal Obsidian app with plugin access. */
|
||||
interface CopilotPluginInternal {
|
||||
projectManager?: {
|
||||
getProjectContext: (id: string) => Promise<unknown>;
|
||||
getCurrentChainManager: () => { createChainWithNewModel: () => Promise<void> };
|
||||
};
|
||||
}
|
||||
|
||||
interface ObsidianAppWithPlugins {
|
||||
plugins: {
|
||||
getPlugin: (id: string) => CopilotPluginInternal | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshVaultIndex() {
|
||||
try {
|
||||
const { getSettings } = await import("@/settings/model");
|
||||
|
|
@ -109,7 +123,7 @@ export async function reloadCurrentProject(app: App) {
|
|||
// Then, trigger the full load and processing logic via ProjectManager.
|
||||
// getProjectContext will call loadProjectContext if markdownNeedsReload is true (which it is now).
|
||||
// loadProjectContext will handle markdown, web, youtube, and other file types (including API calls for new ones).
|
||||
const plugin = (app as any).plugins.getPlugin("copilot");
|
||||
const plugin = (app as unknown as ObsidianAppWithPlugins).plugins.getPlugin("copilot");
|
||||
if (plugin && plugin.projectManager) {
|
||||
await plugin.projectManager.getProjectContext(currentProject.id);
|
||||
// Reason: chain/model config must be refreshed after context reload so the next
|
||||
|
|
@ -159,7 +173,7 @@ export async function forceRebuildCurrentProjectContext(app: App) {
|
|||
// Step 2: Trigger a full reload from scratch.
|
||||
// getProjectContext will call loadProjectContext as the cache is now empty.
|
||||
// loadProjectContext will handle markdown, web, youtube, and all other file types.
|
||||
const plugin = (app as any).plugins.getPlugin("copilot");
|
||||
const plugin = (app as unknown as ObsidianAppWithPlugins).plugins.getPlugin("copilot");
|
||||
if (plugin && plugin.projectManager) {
|
||||
await plugin.projectManager.getProjectContext(currentProject.id);
|
||||
new Notice(
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import { useSettingsValue } from "@/settings/model";
|
|||
import { SelectedTextContext, WebTabContext } from "@/types/message";
|
||||
import { isAllowedFileForNoteContext } from "@/utils";
|
||||
import { CornerDownLeft, Image, Loader2, StopCircle, X } from "lucide-react";
|
||||
import { App, Notice, TFile } from "obsidian";
|
||||
import { App, Notice, TFile, TFolder } from "obsidian";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { $getSelection, $isRangeSelection } from "lexical";
|
||||
import { $getSelection, $isRangeSelection, LexicalEditor as LexicalEditorType } from "lexical";
|
||||
import { ContextControl } from "./ContextControl";
|
||||
import { $removePillsByPath } from "./pills/NotePillNode";
|
||||
import { $removeActiveNotePills } from "./pills/ActiveNotePillNode";
|
||||
|
|
@ -113,7 +113,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
const [contextFolders, setContextFolders] = useState<string[]>(initialContext?.folders || []);
|
||||
const [contextWebTabs, setContextWebTabs] = useState<WebTabContext[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const lexicalEditorRef = useRef<any>(null);
|
||||
const lexicalEditorRef = useRef<LexicalEditorType | null>(null);
|
||||
const [currentModelKey, setCurrentModelKey] = useModelKey();
|
||||
const [currentChain] = useChainType();
|
||||
const [isProjectLoading] = useProjectLoading();
|
||||
|
|
@ -153,7 +153,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
title: pill.getTitle(),
|
||||
faviconUrl: pill.getFaviconUrl(),
|
||||
}));
|
||||
}) as WebTabContext[];
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle states for vault, web search, composer, and autonomous agent
|
||||
|
|
@ -375,7 +375,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
};
|
||||
|
||||
// Unified handler for adding to context (from popover @ mention)
|
||||
const handleAddToContext = (category: string, data: any) => {
|
||||
const handleAddToContext = (category: string, data: TFile | string | TFolder | WebTabContext) => {
|
||||
switch (category) {
|
||||
case "activeNote":
|
||||
// Set active note context flag (no pill needed - context badge shows it)
|
||||
|
|
@ -428,8 +428,13 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
break;
|
||||
case "webTabs":
|
||||
// Badge-only behavior (like notes): add to contextWebTabs state, no pill insertion
|
||||
if (data && typeof data.url === "string") {
|
||||
const normalized = normalizeWebTabContext(data as WebTabContext);
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"url" in data &&
|
||||
typeof (data as { url: unknown }).url === "string"
|
||||
) {
|
||||
const normalized = normalizeWebTabContext(data);
|
||||
if (!normalized) break;
|
||||
|
||||
// If selecting the active web tab, toggle the active badge instead
|
||||
|
|
@ -462,7 +467,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
};
|
||||
|
||||
// Unified handler for removing from context (from context menu badges)
|
||||
const handleRemoveFromContext = (category: string, data: any) => {
|
||||
const handleRemoveFromContext = (category: string, data: string) => {
|
||||
switch (category) {
|
||||
case "activeNote":
|
||||
// Remove active note pill from editor and turn off flag
|
||||
|
|
@ -638,7 +643,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
};
|
||||
}, [app.workspace]);
|
||||
|
||||
const onEditorReady = useCallback((editor: any) => {
|
||||
const onEditorReady = useCallback((editor: LexicalEditorType) => {
|
||||
lexicalEditorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export function ChatSettingsPopover() {
|
|||
* Update model parameters (immediately update UI, delayed save)
|
||||
*/
|
||||
const handleParamChange = useCallback(
|
||||
(field: keyof CustomModel, value: any) => {
|
||||
(field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => {
|
||||
if (!localModel) return;
|
||||
|
||||
const updatedModel = { ...localModel, [field]: value };
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ describe("think block rendering — closing tags are not consumed by indented co
|
|||
});
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).activeDocument = window.document;
|
||||
(window as unknown as Record<string, unknown>).activeDocument = window.document;
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -279,7 +279,7 @@ describe("ChatSingleMessage", () => {
|
|||
});
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).activeDocument = window.document;
|
||||
(window as unknown as Record<string, unknown>).activeDocument = window.document;
|
||||
});
|
||||
|
||||
it("normalizes rendered footnotes for assistant messages", async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
import { SelectedTextContext, WebTabContext } from "@/types/message";
|
||||
import { TFile } from "obsidian";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import { ChatContextMenu } from "./ChatContextMenu";
|
||||
|
||||
interface ChatControlsProps {
|
||||
|
|
@ -16,11 +16,11 @@ interface ChatControlsProps {
|
|||
selectedTextContexts?: SelectedTextContext[];
|
||||
showProgressCard: () => void;
|
||||
showIndexingCard?: () => void;
|
||||
lexicalEditorRef?: React.RefObject<any>;
|
||||
lexicalEditorRef?: React.RefObject<{ focus: () => void }>;
|
||||
|
||||
// Unified handlers
|
||||
onAddToContext: (category: string, data: any) => void;
|
||||
onRemoveFromContext: (category: string, data: any) => void;
|
||||
onAddToContext: (category: string, data: TFile | string | TFolder | WebTabContext) => void;
|
||||
onRemoveFromContext: (category: string, data: string) => void;
|
||||
}
|
||||
|
||||
export const ContextControl: React.FC<ChatControlsProps> = ({
|
||||
|
|
@ -39,12 +39,15 @@ export const ContextControl: React.FC<ChatControlsProps> = ({
|
|||
onAddToContext,
|
||||
onRemoveFromContext,
|
||||
}) => {
|
||||
const handleRemoveContext = (category: string, data: any) => {
|
||||
const handleRemoveContext = (category: string, data: string) => {
|
||||
// Delegate to unified handler
|
||||
onRemoveFromContext(category, data);
|
||||
};
|
||||
|
||||
const handleTypeaheadSelect = (category: string, data: any) => {
|
||||
const handleTypeaheadSelect = (
|
||||
category: string,
|
||||
data: TFile | string | TFolder | WebTabContext
|
||||
) => {
|
||||
// Delegate to unified handler
|
||||
onAddToContext(category, data);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ interface LexicalEditorProps {
|
|||
onWebTabsRemoved?: (removedWebTabs: WebTabContext[]) => void;
|
||||
onActiveWebTabAdded?: () => void;
|
||||
onActiveWebTabRemoved?: () => void;
|
||||
onEditorReady?: (editor: any) => void;
|
||||
onEditorReady?: (editor: LexicalEditorType) => void;
|
||||
onImagePaste?: (files: File[]) => void;
|
||||
onTagSelected?: () => void;
|
||||
isCopilotPlus?: boolean;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function $createFolderPillNode(folderPath: string): FolderPillNode {
|
|||
return new FolderPillNode(folderPath);
|
||||
}
|
||||
|
||||
export function $isFolderPillNode(node: any): node is FolderPillNode {
|
||||
export function $isFolderPillNode(node: LexicalNode): node is FolderPillNode {
|
||||
return node instanceof FolderPillNode;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function $createToolPillNode(toolName: string): ToolPillNode {
|
|||
return new ToolPillNode(toolName);
|
||||
}
|
||||
|
||||
export function $isToolPillNode(node: any): node is ToolPillNode {
|
||||
export function $isToolPillNode(node: LexicalNode): node is ToolPillNode {
|
||||
return node instanceof ToolPillNode;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -187,6 +187,6 @@ export function $removePillsByURL(url: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function $isURLPillNode(node: any): node is URLPillNode {
|
||||
export function $isURLPillNode(node: LexicalNode): node is URLPillNode {
|
||||
return node instanceof URLPillNode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import type { LexicalEditor } from "lexical";
|
||||
|
||||
/**
|
||||
* Props for the FocusPlugin component
|
||||
|
|
@ -8,7 +9,7 @@ interface FocusPluginProps {
|
|||
/** Callback that receives a function to programmatically focus the editor */
|
||||
onFocus: (focusFn: () => void) => void;
|
||||
/** Optional callback that receives the editor instance when ready */
|
||||
onEditorReady?: (editor: any) => void;
|
||||
onEditorReady?: (editor: LexicalEditor) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { $isFolderPillNode, FolderPillNode } from "../pills/FolderPillNode";
|
||||
import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin";
|
||||
import type { LexicalNode } from "lexical";
|
||||
|
||||
/**
|
||||
* Props for the FolderPillSyncPlugin component
|
||||
|
|
@ -17,7 +18,7 @@ interface FolderPillSyncPluginProps {
|
|||
*/
|
||||
const folderPillConfig: PillSyncConfig<string> = {
|
||||
isPillNode: $isFolderPillNode,
|
||||
extractData: (node: any) => (node as FolderPillNode).getFolderPath(),
|
||||
extractData: (node: LexicalNode) => (node as FolderPillNode).getFolderPath(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { $getRoot, LexicalNode } from "lexical";
|
|||
*/
|
||||
export interface PillSyncConfig<T> {
|
||||
/** Function to check if a node is of this pill type */
|
||||
isPillNode: (node: any) => boolean;
|
||||
isPillNode: (node: LexicalNode) => boolean;
|
||||
/** Function to extract data from the pill node */
|
||||
extractData: (node: any) => T;
|
||||
extractData: (node: LexicalNode) => T;
|
||||
/** Function to create a unique identity key for deduplication and removal detection */
|
||||
getKey?: (item: T) => string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,10 +22,13 @@ type NoteData = { path: string; basename: string };
|
|||
*/
|
||||
const notePillConfig: PillSyncConfig<NoteData> = {
|
||||
isPillNode: $isNotePillNode,
|
||||
extractData: (node: { getNotePath: () => string; getNoteTitle: () => string }) => ({
|
||||
path: node.getNotePath(),
|
||||
basename: node.getNoteTitle(),
|
||||
}),
|
||||
extractData: (node) => {
|
||||
const noteNode = node as unknown as { getNotePath: () => string; getNoteTitle: () => string };
|
||||
return {
|
||||
path: noteNode.getNotePath(),
|
||||
basename: noteNode.getNoteTitle(),
|
||||
};
|
||||
},
|
||||
getKey: (note: NoteData) => note.path, // Use path as unique key
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
COMMAND_PRIORITY_CRITICAL,
|
||||
$isElementNode,
|
||||
DecoratorNode,
|
||||
LexicalNode,
|
||||
} from "lexical";
|
||||
|
||||
/**
|
||||
|
|
@ -19,7 +20,7 @@ export interface IPillNode {
|
|||
/**
|
||||
* Check if a node is a pill-like node (any DecoratorNode that implements IPillNode)
|
||||
*/
|
||||
function $isPillNode(node: any): node is DecoratorNode<any> & IPillNode {
|
||||
function $isPillNode(node: LexicalNode): node is DecoratorNode<React.ReactNode> & IPillNode {
|
||||
// Check if it's a DecoratorNode (all pills extend DecoratorNode)
|
||||
if (!(node instanceof DecoratorNode)) {
|
||||
return false;
|
||||
|
|
@ -82,7 +83,7 @@ export function PillDeletionPlugin(): null {
|
|||
// Currently disabled since typeahead adds spaces after pills
|
||||
if (isBackward && anchor.offset === 0) {
|
||||
const previousSibling = anchorNode.getPreviousSibling();
|
||||
if ($isPillNode(previousSibling)) {
|
||||
if (previousSibling && $isPillNode(previousSibling)) {
|
||||
previousSibling.remove();
|
||||
handled = true;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { $isToolPillNode, ToolPillNode } from "../pills/ToolPillNode";
|
||||
import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin";
|
||||
import type { LexicalNode } from "lexical";
|
||||
|
||||
/**
|
||||
* Props for the ToolPillSyncPlugin component
|
||||
|
|
@ -17,7 +18,7 @@ interface ToolPillSyncPluginProps {
|
|||
*/
|
||||
const toolPillConfig: PillSyncConfig<string> = {
|
||||
isPillNode: $isToolPillNode,
|
||||
extractData: (node: any) => (node as ToolPillNode).getToolName(),
|
||||
extractData: (node: LexicalNode) => (node as ToolPillNode).getToolName(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { $isURLPillNode, URLPillNode } from "../pills/URLPillNode";
|
||||
import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin";
|
||||
import type { LexicalNode } from "lexical";
|
||||
|
||||
/**
|
||||
* Props for the URLPillSyncPlugin component
|
||||
|
|
@ -17,7 +18,7 @@ interface URLPillSyncPluginProps {
|
|||
*/
|
||||
const urlPillConfig: PillSyncConfig<string> = {
|
||||
isPillNode: $isURLPillNode,
|
||||
extractData: (node: any) => (node as URLPillNode).getURL(),
|
||||
extractData: (node: LexicalNode) => (node as URLPillNode).getURL(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe("parseTextForPills", () => {
|
|||
});
|
||||
|
||||
// Mock TFile constructor to set properties
|
||||
MockTFile.mockImplementation(function (this: any) {
|
||||
MockTFile.mockImplementation(function (this: Record<string, unknown>) {
|
||||
this.basename = "Valid Note";
|
||||
this.path = "Valid Note.md";
|
||||
});
|
||||
|
|
@ -279,7 +279,7 @@ describe("parseTextForPills", () => {
|
|||
return null;
|
||||
});
|
||||
|
||||
MockTFile.mockImplementation(function (this: any) {
|
||||
MockTFile.mockImplementation(function (this: Record<string, unknown>) {
|
||||
this.basename = "Test Note";
|
||||
this.path = "Test Note.md";
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { logError } from "@/logger";
|
|||
import { App, Modal, Setting, TFile } from "obsidian";
|
||||
|
||||
export class SourcesModal extends Modal {
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[];
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[]
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[]
|
||||
) {
|
||||
super(app);
|
||||
this.sources = sources;
|
||||
|
|
@ -24,7 +24,7 @@ export class SourcesModal extends Modal {
|
|||
|
||||
private createSourceList(
|
||||
container: HTMLElement,
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[]
|
||||
sources: { title: string; path: string; score: number; explanation?: unknown }[]
|
||||
) {
|
||||
const list = container.createEl("ul");
|
||||
list.addClass("tw-list-none", "tw-p-0");
|
||||
|
|
@ -58,7 +58,14 @@ export class SourcesModal extends Modal {
|
|||
const filePath = source.path || source.title;
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (file instanceof TFile) {
|
||||
const dragManager = (this.app as any).dragManager;
|
||||
const dragManager = (
|
||||
this.app as unknown as {
|
||||
dragManager?: {
|
||||
dragLink: (e: DragEvent, text: string) => unknown;
|
||||
onDragStart: (e: DragEvent, data: unknown) => void;
|
||||
};
|
||||
}
|
||||
).dragManager;
|
||||
if (!dragManager) return;
|
||||
const linkText = this.app.metadataCache.fileToLinktext(file, "");
|
||||
const dragData = dragManager.dragLink(e, linkText);
|
||||
|
|
@ -99,7 +106,7 @@ export class SourcesModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
private addExplanation(container: HTMLElement, explanation: any): HTMLElement {
|
||||
private addExplanation(container: HTMLElement, explanation: unknown): HTMLElement {
|
||||
const explanationDiv = container.createDiv({ cls: "search-explanation" });
|
||||
explanationDiv.addClass(
|
||||
"tw-ml-[2.5em]",
|
||||
|
|
@ -111,11 +118,13 @@ export class SourcesModal extends Modal {
|
|||
"tw-border-l-border"
|
||||
);
|
||||
|
||||
// Cast to a typed record for safe property access
|
||||
const exp = explanation as Record<string, unknown>;
|
||||
const details: string[] = [];
|
||||
|
||||
// Add lexical matches
|
||||
if (explanation.lexicalMatches && explanation.lexicalMatches.length > 0) {
|
||||
const lexicalMatches = explanation.lexicalMatches as { field: string; query: string }[];
|
||||
if (exp.lexicalMatches && (exp.lexicalMatches as unknown[]).length > 0) {
|
||||
const lexicalMatches = exp.lexicalMatches as { field: string; query: string }[];
|
||||
const fields = new Set(lexicalMatches.map((m) => m.field));
|
||||
const queries = new Set(lexicalMatches.map((m) => m.query));
|
||||
details.push(
|
||||
|
|
@ -124,21 +133,27 @@ export class SourcesModal extends Modal {
|
|||
}
|
||||
|
||||
// Add semantic score
|
||||
if (explanation.semanticScore !== undefined && explanation.semanticScore > 0) {
|
||||
details.push(`Semantic: ${(explanation.semanticScore * 100).toFixed(1)}% similarity`);
|
||||
if (exp.semanticScore !== undefined && (exp.semanticScore as number) > 0) {
|
||||
details.push(`Semantic: ${((exp.semanticScore as number) * 100).toFixed(1)}% similarity`);
|
||||
}
|
||||
|
||||
// Add folder boost
|
||||
if (explanation.folderBoost) {
|
||||
if (exp.folderBoost) {
|
||||
const fb = exp.folderBoost as { boostFactor: number; documentCount: number; folder?: string };
|
||||
details.push(
|
||||
`Folder boost: ${explanation.folderBoost.boostFactor.toFixed(2)}x (${explanation.folderBoost.documentCount} docs in ${explanation.folderBoost.folder || "root"})`
|
||||
`Folder boost: ${fb.boostFactor.toFixed(2)}x (${fb.documentCount} docs in ${fb.folder || "root"})`
|
||||
);
|
||||
}
|
||||
|
||||
// Add graph connections (new query-aware boost)
|
||||
if (explanation.graphConnections) {
|
||||
const gc = explanation.graphConnections;
|
||||
const connectionParts = [];
|
||||
if (exp.graphConnections) {
|
||||
const gc = exp.graphConnections as {
|
||||
backlinks: number;
|
||||
coCitations: number;
|
||||
sharedTags: number;
|
||||
score: number;
|
||||
};
|
||||
const connectionParts: string[] = [];
|
||||
if (gc.backlinks > 0) connectionParts.push(`${gc.backlinks} backlinks`);
|
||||
if (gc.coCitations > 0) connectionParts.push(`${gc.coCitations} co-citations`);
|
||||
if (gc.sharedTags > 0) connectionParts.push(`${gc.sharedTags} shared tags`);
|
||||
|
|
@ -151,16 +166,15 @@ export class SourcesModal extends Modal {
|
|||
}
|
||||
|
||||
// Add old graph boost (if still present for backwards compatibility)
|
||||
if (explanation.graphBoost && !explanation.graphConnections) {
|
||||
details.push(
|
||||
`Graph boost: ${explanation.graphBoost.boostFactor.toFixed(2)}x (${explanation.graphBoost.connections} connections)`
|
||||
);
|
||||
if (exp.graphBoost && !exp.graphConnections) {
|
||||
const gb = exp.graphBoost as { boostFactor: number; connections: number };
|
||||
details.push(`Graph boost: ${gb.boostFactor.toFixed(2)}x (${gb.connections} connections)`);
|
||||
}
|
||||
|
||||
// Add base vs final score if boosted
|
||||
if (explanation.baseScore !== explanation.finalScore) {
|
||||
if (exp.baseScore !== exp.finalScore) {
|
||||
details.push(
|
||||
`Score: ${explanation.baseScore.toFixed(4)} → ${explanation.finalScore.toFixed(4)}`
|
||||
`Score: ${(exp.baseScore as number).toFixed(4)} → ${(exp.finalScore as number).toFixed(4)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ function AddProjectModalContent({
|
|||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | string[] | Record<string, any>
|
||||
value: string | number | string[] | Record<string, unknown>
|
||||
) => {
|
||||
setFormData((prev) => {
|
||||
if (typeof value === "string") {
|
||||
|
|
@ -126,7 +126,7 @@ function AddProjectModalContent({
|
|||
}
|
||||
}
|
||||
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
||||
value = (value as string[]).map((item) => item.trim()).filter(Boolean);
|
||||
value = value.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (field.includes(".")) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles"
|
|||
type ActiveItem = string | null;
|
||||
|
||||
interface SectionHeaderProps {
|
||||
IconComponent: React.ComponentType<any>;
|
||||
IconComponent: React.ComponentType<{ className?: string }>;
|
||||
title: string;
|
||||
iconColorClassName: string;
|
||||
onAddClick: () => void;
|
||||
|
|
@ -112,7 +112,7 @@ interface SectionItem {
|
|||
|
||||
interface SectionListProps {
|
||||
title: string;
|
||||
IconComponent: React.ComponentType<any>;
|
||||
IconComponent: React.ComponentType<{ className?: string }>;
|
||||
iconColorClassName: string;
|
||||
items: SectionItem[];
|
||||
activeItem: string | null;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const PARAM_RANGES = {
|
|||
interface ModelParametersEditorProps {
|
||||
model: CustomModel;
|
||||
settings: CopilotSettings;
|
||||
onChange: (field: keyof CustomModel, value: any) => void;
|
||||
onChange: (field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => void;
|
||||
onReset?: (field: keyof CustomModel) => void;
|
||||
showTokenLimit?: boolean; // Whether to show Token limit, defaults to true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { useSortable } from "@dnd-kit/sortable";
|
|||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { ChevronDown, ChevronRight, GripVertical, MoreVertical } from "lucide-react";
|
||||
|
||||
export interface MobileCardDropdownAction<T = any> {
|
||||
export interface MobileCardDropdownAction<T = object> {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: (item: T) => void | Promise<void>;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ Here's a summary.`;
|
|||
|
||||
const result = compactAssistantOutput(output, { verbatimThreshold: 1000 });
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const resultArray = result as any[];
|
||||
const resultArray = result as Array<{ text: string; type: string }>;
|
||||
expect(resultArray[0].text.length).toBeLessThan(textItem.text.length);
|
||||
expect(resultArray[1]).toEqual(imageItem); // Image unchanged
|
||||
});
|
||||
|
|
|
|||
|
|
@ -631,7 +631,7 @@ Here's what I found in the note.`;
|
|||
const result = compactChatHistoryContent(content, { verbatimThreshold: 1000 });
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const resultArray = result as any[];
|
||||
const resultArray = result as Array<{ type: string; text: string }>;
|
||||
expect(resultArray[0].type).toBe("text");
|
||||
expect(resultArray[0].text.length).toBeLessThan(originalText.length);
|
||||
expect(resultArray[1]).toEqual(content[1]); // Image unchanged
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ jest.mock("@/chainFactory", () => ({
|
|||
import { ContextProcessor } from "@/contextProcessor";
|
||||
import { DATAVIEW_BLOCK_TAG } from "@/constants";
|
||||
|
||||
// Typed helper to access plugins on the mocked global app
|
||||
function getPlugins(): Record<string, unknown> {
|
||||
return (window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).plugins
|
||||
.plugins;
|
||||
}
|
||||
|
||||
function setPlugins(plugins: Record<string, unknown>): void {
|
||||
(window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).plugins.plugins =
|
||||
plugins;
|
||||
}
|
||||
|
||||
// Mock the global app object for Dataview plugin access
|
||||
window.app = {
|
||||
plugins: {
|
||||
|
|
@ -25,7 +36,7 @@ describe("ContextProcessor - Dataview Integration", () => {
|
|||
beforeEach(() => {
|
||||
contextProcessor = ContextProcessor.getInstance();
|
||||
// Reset plugins for each test
|
||||
(window.app as any).plugins.plugins = {};
|
||||
setPlugins({});
|
||||
// Save and mock console.error to suppress expected error messages
|
||||
originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
|
@ -44,7 +55,7 @@ describe("ContextProcessor - Dataview Integration", () => {
|
|||
});
|
||||
|
||||
it("should return content unchanged when Dataview API is not available", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {};
|
||||
getPlugins().dataview = {};
|
||||
const content = "```dataview\nLIST\n```";
|
||||
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
||||
expect(result).toBe(content);
|
||||
|
|
@ -54,7 +65,7 @@ describe("ContextProcessor - Dataview Integration", () => {
|
|||
describe("processDataviewBlocks - Regex Pattern Matching", () => {
|
||||
beforeEach(() => {
|
||||
// Mock successful Dataview plugin with API
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -105,7 +116,7 @@ describe("ContextProcessor - Dataview Integration", () => {
|
|||
|
||||
describe("processDataviewBlocks - Multiple Blocks", () => {
|
||||
beforeEach(() => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest
|
||||
.fn()
|
||||
|
|
@ -172,7 +183,7 @@ LIST
|
|||
|
||||
describe("processDataviewBlocks - Query Execution", () => {
|
||||
it("should include both original query and executed results", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -194,7 +205,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should handle query timeout gracefully", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockImplementation(
|
||||
() =>
|
||||
|
|
@ -213,7 +224,7 @@ LIST
|
|||
}, 10000); // Increase test timeout to 10s
|
||||
|
||||
it("should handle query execution errors", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: false,
|
||||
|
|
@ -229,7 +240,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should handle dataviewjs with unsupported message", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn(),
|
||||
},
|
||||
|
|
@ -244,7 +255,7 @@ LIST
|
|||
|
||||
describe("processDataviewBlocks - Result Formatting", () => {
|
||||
it("should format LIST results correctly", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -265,7 +276,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should format TABLE results correctly", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -291,7 +302,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should format TASK results correctly", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -314,7 +325,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should handle empty results", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -333,7 +344,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should handle null values gracefully", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -354,7 +365,7 @@ LIST
|
|||
});
|
||||
|
||||
it("should handle arrays in values", async () => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
@ -375,7 +386,7 @@ LIST
|
|||
|
||||
describe("processDataviewBlocks - Edge Cases", () => {
|
||||
beforeEach(() => {
|
||||
(window.app as any).plugins.plugins.dataview = {
|
||||
getPlugins().dataview = {
|
||||
api: {
|
||||
query: jest.fn().mockResolvedValue({
|
||||
successful: true,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const createMockFile = (path: string): TFile =>
|
|||
describe("ContextProcessor - Embedded Notes", () => {
|
||||
let contextProcessor: ContextProcessor;
|
||||
let vault: Vault;
|
||||
let fileParserManager: any;
|
||||
let fileParserManager: unknown;
|
||||
let fileCaches: FileCacheMap;
|
||||
let fileContents: FileContentMap;
|
||||
let fileIndex: Map<string, TFile>;
|
||||
|
|
@ -41,7 +41,7 @@ describe("ContextProcessor - Embedded Notes", () => {
|
|||
getFileCache: jest.fn((file: TFile) => fileCaches[file.path] ?? {}),
|
||||
};
|
||||
|
||||
(window as any).app = {
|
||||
(window as unknown as Record<string, unknown>).app = {
|
||||
metadataCache: metadataCacheMock,
|
||||
};
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ describe("ContextProcessor - Embedded Notes", () => {
|
|||
},
|
||||
} as unknown as Vault;
|
||||
|
||||
(vault as any).getAbstractFileByPath = jest.fn();
|
||||
(vault as Vault & Record<string, unknown>).getAbstractFileByPath = jest.fn();
|
||||
|
||||
fileParserManager = {
|
||||
supportsExtension: jest.fn(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,31 @@ import {
|
|||
YOUTUBE_VIDEO_CONTEXT_TAG,
|
||||
} from "./constants";
|
||||
|
||||
/** Minimal typing for the Dataview plugin API (third-party plugin, no official types). */
|
||||
interface DataviewApi {
|
||||
query(
|
||||
query: string,
|
||||
sourcePath: string
|
||||
): Promise<{ successful: boolean; error?: string; value: DataviewResult }>;
|
||||
}
|
||||
|
||||
interface DataviewResult {
|
||||
type: string;
|
||||
values: DataviewRow[];
|
||||
headers?: string[];
|
||||
}
|
||||
|
||||
type DataviewRow = unknown[] | DataviewTaskItem | DataviewLinkLike;
|
||||
|
||||
interface DataviewTaskItem {
|
||||
completed: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface DataviewLinkLike {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface EmbeddedLinkTarget {
|
||||
path: string | null;
|
||||
heading?: string;
|
||||
|
|
@ -80,7 +105,9 @@ export class ContextProcessor {
|
|||
*/
|
||||
async processDataviewBlocks(content: string, sourcePath: string): Promise<string> {
|
||||
// Check if Dataview plugin is available
|
||||
const dataviewPlugin = (app as any).plugins?.plugins?.dataview;
|
||||
const dataviewPlugin = (
|
||||
app as unknown as { plugins?: { plugins?: { dataview?: { api?: DataviewApi } } } }
|
||||
).plugins?.plugins?.dataview;
|
||||
if (!dataviewPlugin) {
|
||||
return content; // Dataview not installed, return content as-is
|
||||
}
|
||||
|
|
@ -132,7 +159,7 @@ export class ContextProcessor {
|
|||
* Execute a Dataview query and format the results
|
||||
*/
|
||||
private async executeDataviewQuery(
|
||||
dataviewApi: any,
|
||||
dataviewApi: DataviewApi,
|
||||
query: string,
|
||||
queryType: string,
|
||||
sourcePath: string
|
||||
|
|
@ -146,7 +173,7 @@ export class ContextProcessor {
|
|||
const result = await dataviewApi.query(query, sourcePath);
|
||||
|
||||
if (!result.successful) {
|
||||
throw new Error((result.error as string) || "Query failed");
|
||||
throw new Error(result.error ?? "Query failed");
|
||||
}
|
||||
|
||||
// Format results based on type
|
||||
|
|
@ -156,29 +183,29 @@ export class ContextProcessor {
|
|||
/**
|
||||
* Format Dataview query results into readable text
|
||||
*/
|
||||
private formatDataviewResult(result: any): string {
|
||||
private formatDataviewResult(result: DataviewResult): string {
|
||||
if (!result) {
|
||||
return "No results";
|
||||
}
|
||||
|
||||
// Handle different result types
|
||||
if (result.type === "list") {
|
||||
return this.formatDataviewList(result.values as any[]);
|
||||
return this.formatDataviewList(result.values);
|
||||
} else if (result.type === "table") {
|
||||
return this.formatDataviewTable(result.headers as string[], result.values as any[][]);
|
||||
return this.formatDataviewTable(result.headers ?? [], result.values as unknown[][]);
|
||||
} else if (result.type === "task") {
|
||||
return this.formatDataviewTasks(result.values as any[]);
|
||||
return this.formatDataviewTasks(result.values as DataviewTaskItem[]);
|
||||
} else if (Array.isArray(result)) {
|
||||
return result.map((item) => this.formatDataviewValue(item)).join("\n");
|
||||
return (result as unknown[]).map((item) => this.formatDataviewValue(item)).join("\n");
|
||||
}
|
||||
|
||||
return String(result);
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Dataview list results
|
||||
*/
|
||||
private formatDataviewList(values: any[]): string {
|
||||
private formatDataviewList(values: DataviewRow[]): string {
|
||||
if (!values || values.length === 0) {
|
||||
return "No results";
|
||||
}
|
||||
|
|
@ -188,7 +215,7 @@ export class ContextProcessor {
|
|||
/**
|
||||
* Format Dataview table results
|
||||
*/
|
||||
private formatDataviewTable(headers: string[], rows: any[][]): string {
|
||||
private formatDataviewTable(headers: string[], rows: unknown[][]): string {
|
||||
if (!rows || rows.length === 0) {
|
||||
return "No results";
|
||||
}
|
||||
|
|
@ -207,14 +234,14 @@ export class ContextProcessor {
|
|||
/**
|
||||
* Format Dataview task results
|
||||
*/
|
||||
private formatDataviewTasks(tasks: any[]): string {
|
||||
private formatDataviewTasks(tasks: DataviewTaskItem[]): string {
|
||||
if (!tasks || tasks.length === 0) {
|
||||
return "No results";
|
||||
}
|
||||
return tasks
|
||||
.map((task) => {
|
||||
const checkbox = task.completed ? "[x]" : "[ ]";
|
||||
return `- ${checkbox} ${this.formatDataviewValue(task.text || task)}`;
|
||||
return `- ${checkbox} ${this.formatDataviewValue(task.text ?? task)}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
|
@ -222,22 +249,27 @@ export class ContextProcessor {
|
|||
/**
|
||||
* Format individual Dataview values
|
||||
*/
|
||||
private formatDataviewValue(value: any): string {
|
||||
private formatDataviewValue(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Handle links
|
||||
if (value && typeof value === "object" && value.path) {
|
||||
return `[[${value.path}]]`;
|
||||
if (value && typeof value === "object" && "path" in value) {
|
||||
return `[[${(value as DataviewLinkLike).path}]]`;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => this.formatDataviewValue(v)).join(", ");
|
||||
return (value as unknown[]).map((v) => this.formatDataviewValue(v)).join(", ");
|
||||
}
|
||||
|
||||
return String(value);
|
||||
if (typeof value === "object") {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
// value is a primitive (string, number, boolean, bigint) at this point
|
||||
return `${value as string | number | boolean | bigint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { ContextManager } from "./ContextManager";
|
|||
import { MessageRepository } from "./MessageRepository";
|
||||
import { ChatPersistenceManager } from "./ChatPersistenceManager";
|
||||
import { ACTIVE_WEB_TAB_MARKER, USER_SENDER } from "@/constants";
|
||||
import { MessageContent } from "@/imageProcessing/imageProcessor";
|
||||
import { TFile, Vault } from "obsidian";
|
||||
import { getWebViewerService } from "@/services/webViewerService/webViewerServiceSingleton";
|
||||
import {
|
||||
|
|
@ -416,7 +417,7 @@ export class ChatManager {
|
|||
chainType: ChainType,
|
||||
includeActiveNote: boolean = false,
|
||||
includeActiveWebTab: boolean = false,
|
||||
content?: any[],
|
||||
content?: MessageContent[],
|
||||
updateLoadingMessage?: (message: string) => void
|
||||
): Promise<string> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { logError, logInfo, logWarn } from "@/logger";
|
|||
import { sanitizeVaultPathSegment } from "@/projects/projectUtils";
|
||||
import { filterChatHistoryFiles, readChatPathProjectId } from "@/utils/chatHistoryUtils";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { ChatMessage } from "@/types/message";
|
||||
import { ChatMessage, MessageContext } from "@/types/message";
|
||||
import {
|
||||
ensureFolderExists,
|
||||
extractTextFromChunk,
|
||||
|
|
@ -419,7 +419,7 @@ export class ChatPersistenceManager {
|
|||
const contentLines = fullContent.split("\n");
|
||||
let messageText = fullContent;
|
||||
let timestamp = "Unknown time";
|
||||
let contextInfo: any = undefined;
|
||||
let contextInfo: MessageContext | undefined = undefined;
|
||||
|
||||
// Check for context and timestamp lines
|
||||
let endIndex = contentLines.length;
|
||||
|
|
@ -492,8 +492,8 @@ export class ChatPersistenceManager {
|
|||
/**
|
||||
* Parse context string back into context object
|
||||
*/
|
||||
private parseContextString(contextStr: string): any {
|
||||
const context: any = {
|
||||
private parseContextString(contextStr: string): MessageContext | undefined {
|
||||
const context: MessageContext = {
|
||||
notes: [],
|
||||
urls: [],
|
||||
tags: [],
|
||||
|
|
@ -581,9 +581,9 @@ export class ChatPersistenceManager {
|
|||
if (
|
||||
context.notes.length > 0 ||
|
||||
context.urls.length > 0 ||
|
||||
context.tags.length > 0 ||
|
||||
context.folders.length > 0 ||
|
||||
context.webTabs.length > 0
|
||||
(context.tags?.length ?? 0) > 0 ||
|
||||
(context.folders?.length ?? 0) > 0 ||
|
||||
(context.webTabs?.length ?? 0) > 0
|
||||
) {
|
||||
return context;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ describe("MessageRepository", () => {
|
|||
messageRepo.addMessage("Hello", "Hello", "user");
|
||||
|
||||
// Make message invisible by directly accessing internal array
|
||||
const internalMessages = (messageRepo as any).messages as StoredMessage[];
|
||||
const internalMessages = (messageRepo as unknown as { messages: StoredMessage[] }).messages;
|
||||
internalMessages[0].isVisible = false;
|
||||
|
||||
const messages = messageRepo.getDisplayMessages();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PromptContextEnvelope } from "@/context/PromptContextTypes";
|
||||
import { MessageContent } from "@/imageProcessing/imageProcessor";
|
||||
import { formatDateTime } from "@/utils";
|
||||
import { ChatMessage, MessageContext, NewChatMessage, StoredMessage } from "@/types/message";
|
||||
import { logInfo } from "@/logger";
|
||||
|
|
@ -34,14 +35,14 @@ export class MessageRepository {
|
|||
processedText: string,
|
||||
sender: string,
|
||||
context?: MessageContext,
|
||||
content?: any[]
|
||||
content?: MessageContent[]
|
||||
): string;
|
||||
addMessage(
|
||||
messageOrDisplayText: NewChatMessage | string,
|
||||
processedText?: string,
|
||||
sender?: string,
|
||||
context?: MessageContext,
|
||||
content?: any[]
|
||||
content?: MessageContent[]
|
||||
): string {
|
||||
// If first parameter is a ChatMessage object
|
||||
if (typeof messageOrDisplayText === "object") {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ export function err2String(err: unknown, stack = false): string {
|
|||
try {
|
||||
if (err instanceof Error) {
|
||||
const cause = (err as { cause?: unknown }).cause;
|
||||
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause as any) : "";
|
||||
const causeMsg =
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: cause != null
|
||||
? typeof cause === "string"
|
||||
? cause
|
||||
: (JSON.stringify(cause) ?? "")
|
||||
: "";
|
||||
const stackStr = stack && err.stack ? err.stack : "";
|
||||
const parts = [err.message];
|
||||
if (causeMsg) parts.push(`more message: ${causeMsg}`);
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ export function useStreamingChatSession(
|
|||
const stream = await chainWithSignal.stream({ input: prompt });
|
||||
|
||||
for await (const chunk of stream) {
|
||||
thinkStreamer.processChunk(chunk);
|
||||
thinkStreamer.processChunk(chunk as Parameters<typeof thinkStreamer.processChunk>[0]);
|
||||
if (abortController.signal.aborted) break;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import plugin from "tailwindcss/plugin";
|
||||
import type { CSSRuleObject } from "tailwindcss/types/config";
|
||||
|
||||
// Types
|
||||
interface ColorValue {
|
||||
|
|
@ -25,7 +26,8 @@ const getPropertyPrefix = (property: ColorProperty): string => {
|
|||
|
||||
// Utility Generator
|
||||
const generateUtility =
|
||||
(e: any) => (property: ColorProperty, name: string, color: string, opacity: number) => {
|
||||
(e: (className: string) => string) =>
|
||||
(property: ColorProperty, name: string, color: string, opacity: number) => {
|
||||
const prefix = getPropertyPrefix(property);
|
||||
const className = `${prefix}-${name}/${opacity}`;
|
||||
|
||||
|
|
@ -37,24 +39,26 @@ const generateUtility =
|
|||
};
|
||||
|
||||
// Color Processing
|
||||
const generateAllUtilities = (e: any) => (color: string, name: string, opacity: number) => {
|
||||
const properties: ColorProperty[] = ["background-color", "border-color", "color"];
|
||||
const utilities = properties.map((property) =>
|
||||
generateUtility(e)(property, name, color, opacity)
|
||||
);
|
||||
const generateAllUtilities =
|
||||
(e: (className: string) => string) => (color: string, name: string, opacity: number) => {
|
||||
const properties: ColorProperty[] = ["background-color", "border-color", "color"];
|
||||
const utilities = properties.map((property) =>
|
||||
generateUtility(e)(property, name, color, opacity)
|
||||
);
|
||||
|
||||
return Object.assign({}, ...utilities) as Record<string, unknown>;
|
||||
};
|
||||
return Object.assign({}, ...utilities) as CSSRuleObject;
|
||||
};
|
||||
|
||||
const generateOpacityClasses =
|
||||
(e: any, opacityUtilities: Record<string, any>) => (color: string, name: string) => {
|
||||
(e: (className: string) => string, opacityUtilities: CSSRuleObject) =>
|
||||
(color: string, name: string) => {
|
||||
Array.from({ length: 10 }, (_, i) => (i + 1) * 10).forEach((opacity) => {
|
||||
Object.assign(opacityUtilities, generateAllUtilities(e)(color, name, opacity));
|
||||
});
|
||||
};
|
||||
|
||||
const processColorObject =
|
||||
(e: any, opacityUtilities: Record<string, any>) =>
|
||||
(e: (className: string) => string, opacityUtilities: CSSRuleObject) =>
|
||||
(colorValue: string | ColorValue, baseName: string, parentPath: string[] = []) => {
|
||||
const currentPath = [...parentPath, baseName];
|
||||
|
||||
|
|
@ -88,7 +92,7 @@ const processColorObject =
|
|||
*/
|
||||
export const colorOpacityPlugin = plugin((api) => {
|
||||
const { theme, e } = api;
|
||||
const opacityUtilities: Record<string, any> = {};
|
||||
const opacityUtilities: CSSRuleObject = {};
|
||||
|
||||
// 处理所有颜色相关的主题配置
|
||||
const processThemeColors = (themeKey: string, prefix?: string) => {
|
||||
|
|
|
|||
|
|
@ -912,7 +912,11 @@ export default class CopilotPlugin extends Plugin {
|
|||
await this.handleNewChat();
|
||||
}
|
||||
|
||||
async customSearchDB(query: string, salientTerms: string[], textWeight: number): Promise<any[]> {
|
||||
async customSearchDB(
|
||||
query: string,
|
||||
salientTerms: string[],
|
||||
textWeight: number
|
||||
): Promise<{ content: string; metadata: Record<string, unknown> }[]> {
|
||||
const settings = getSettings();
|
||||
|
||||
// Run FilterRetriever for guaranteed title/tag matches
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { UserMemoryManager } from "./UserMemoryManager";
|
|||
import { App, TFile, Vault } from "obsidian";
|
||||
import { ChatMessage } from "@/types/message";
|
||||
import { logError, logWarn } from "@/logger";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { CopilotSettings, getSettings } from "@/settings/model";
|
||||
import { ensureFolderExists } from "@/utils";
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { AIMessageChunk } from "@langchain/core/messages";
|
||||
|
|
@ -48,7 +48,7 @@ describe("UserMemoryManager", () => {
|
|||
let mockApp: jest.Mocked<App>;
|
||||
let mockVault: jest.Mocked<Vault>;
|
||||
let mockChatModel: jest.Mocked<BaseChatModel>;
|
||||
let mockSettings: any;
|
||||
let mockSettings: Partial<CopilotSettings>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface MiyoServiceConfig {
|
|||
pid: number;
|
||||
}
|
||||
|
||||
type NodeRequire = (id: string) => any;
|
||||
type NodeRequire = (id: string) => unknown;
|
||||
type ServiceConfigReadResult = MiyoServiceConfig | "missing" | null;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -134,7 +134,9 @@ export function useIsPlusUser(): boolean | undefined {
|
|||
* Check if the user is a Plus user.
|
||||
* When self-host mode is valid, this returns true to allow offline usage.
|
||||
*/
|
||||
export async function checkIsPlusUser(context?: Record<string, any>): Promise<boolean | undefined> {
|
||||
export async function checkIsPlusUser(
|
||||
context?: Record<string, unknown>
|
||||
): Promise<boolean | undefined> {
|
||||
// Self-host mode with valid plan validation bypasses license check
|
||||
if (isSelfHostModeValid()) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const LEGACY_INDEX_SUFFIX = ".json";
|
|||
export interface ChunkMetadata {
|
||||
numPartitions: number;
|
||||
vectorLength: number;
|
||||
schema: any;
|
||||
schema: Record<string, string>;
|
||||
lastModified: number;
|
||||
documentPartitions: Record<string, number>;
|
||||
}
|
||||
|
|
@ -54,10 +54,10 @@ export class ChunkedStorage {
|
|||
}
|
||||
|
||||
private distributeDocumentsToPartitions(
|
||||
documents: any[],
|
||||
documents: Record<string, unknown>[],
|
||||
numPartitions: number
|
||||
): Map<number, any[]> {
|
||||
const partitions = new Map<number, any[]>();
|
||||
): Map<number, Record<string, unknown>[]> {
|
||||
const partitions = new Map<number, Record<string, unknown>[]>();
|
||||
const documentPartitions: Record<string, number> = {};
|
||||
|
||||
for (let i = 0; i < numPartitions; i++) {
|
||||
|
|
@ -106,6 +106,7 @@ export class ChunkedStorage {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is required here as it controls Orama's type inference
|
||||
async saveDatabase(db: Orama<any>): Promise<void> {
|
||||
try {
|
||||
const rawData: RawData = save(db);
|
||||
|
|
@ -125,10 +126,10 @@ export class ChunkedStorage {
|
|||
}
|
||||
|
||||
// NOTE: Orama RawData docs can be either an array or an object
|
||||
const docsData = (rawData as any).docs?.docs;
|
||||
const rawDocs: any[] = Array.isArray(docsData)
|
||||
? docsData
|
||||
: Object.values((docsData as Record<string, unknown>) || {});
|
||||
const docsData = (rawData as unknown as { docs?: { docs?: unknown } }).docs?.docs;
|
||||
const rawDocs: Record<string, unknown>[] = Array.isArray(docsData)
|
||||
? (docsData as Record<string, unknown>[])
|
||||
: (Object.values((docsData as Record<string, unknown>) || {}) as Record<string, unknown>[]);
|
||||
|
||||
if (getSettings().debug) {
|
||||
logInfo(`Starting save with ${rawDocs.length ?? 0} total documents`);
|
||||
|
|
@ -161,7 +162,7 @@ export class ChunkedStorage {
|
|||
schema: db.schema,
|
||||
lastModified: Date.now(),
|
||||
documentPartitions: Object.fromEntries(
|
||||
rawDocs.map((doc: any) => [
|
||||
rawDocs.map((doc) => [
|
||||
doc.id,
|
||||
this.assignDocumentToPartition(String(doc.id), numPartitions),
|
||||
])
|
||||
|
|
@ -174,7 +175,7 @@ export class ChunkedStorage {
|
|||
...rawData,
|
||||
docs: { docs: {}, count: 0 },
|
||||
index: {
|
||||
...(rawData as any).index,
|
||||
...(rawData as unknown as { index: Record<string, unknown> }).index,
|
||||
vectorIndexes: undefined,
|
||||
},
|
||||
};
|
||||
|
|
@ -186,13 +187,24 @@ export class ChunkedStorage {
|
|||
index: {
|
||||
vectorIndexes: {
|
||||
embedding: {
|
||||
size: (rawData as any).index.vectorIndexes.embedding.size,
|
||||
size: (
|
||||
rawData as unknown as {
|
||||
index: {
|
||||
vectorIndexes: {
|
||||
embedding: { size: number; vectors: Record<string, unknown> };
|
||||
};
|
||||
};
|
||||
}
|
||||
).index.vectorIndexes.embedding.size,
|
||||
vectors: Object.fromEntries(
|
||||
Object.entries(
|
||||
(rawData as any).index.vectorIndexes.embedding.vectors as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
(
|
||||
rawData as unknown as {
|
||||
index: {
|
||||
vectorIndexes: { embedding: { vectors: Record<string, unknown> } };
|
||||
};
|
||||
}
|
||||
).index.vectorIndexes.embedding.vectors
|
||||
).filter(([id]) => docs.some((doc) => doc.id === id))
|
||||
),
|
||||
},
|
||||
|
|
@ -234,6 +246,7 @@ export class ChunkedStorage {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is required here as it controls Orama's type inference
|
||||
async loadDatabase(): Promise<Orama<any>> {
|
||||
try {
|
||||
const legacyPath = this.getLegacyPath();
|
||||
|
|
@ -241,7 +254,7 @@ export class ChunkedStorage {
|
|||
// Try loading legacy format first
|
||||
if (await this.app.vault.adapter.exists(legacyPath)) {
|
||||
const legacyData = JSON.parse(await this.app.vault.adapter.read(legacyPath)) as RawData & {
|
||||
schema?: any;
|
||||
schema?: Record<string, string>;
|
||||
};
|
||||
if (!legacyData?.schema) {
|
||||
throw new CustomError("Invalid legacy database format");
|
||||
|
|
@ -294,14 +307,14 @@ export class ChunkedStorage {
|
|||
}
|
||||
|
||||
// Create new docs object based on internalDocumentIDStore order
|
||||
const orderedDocs: Record<string, any> = {};
|
||||
const orderedDocs: Record<string, unknown> = {};
|
||||
let nextDocId = 1;
|
||||
|
||||
for (const internalId of mergedData.internalDocumentIDStore.internalIdToId) {
|
||||
// Find document in any chunk
|
||||
const doc = allChunks
|
||||
.flatMap((chunk) => Object.values(chunk.docs.docs as Record<string, unknown>))
|
||||
.find((doc: any) => doc.id === internalId);
|
||||
.find((doc) => (doc as Record<string, unknown>).id === internalId);
|
||||
|
||||
if (doc) {
|
||||
orderedDocs[nextDocId.toString()] = doc;
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ export interface OramaDocument {
|
|||
tags: string[];
|
||||
extension: string;
|
||||
nchars: number;
|
||||
metadata: Record<string, any>;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class DBOperations {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
private oramaDb: Orama<any> | undefined;
|
||||
private chunkedStorage: ChunkedStorage | undefined;
|
||||
private isInitialized = false;
|
||||
|
|
@ -84,6 +85,7 @@ export class DBOperations {
|
|||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
async initializeDB(embeddingInstance: Embeddings | undefined): Promise<Orama<any> | undefined> {
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
|
|
@ -209,6 +211,7 @@ export class DBOperations {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public getDb(): Orama<any> | undefined {
|
||||
if (!this.oramaDb) {
|
||||
console.warn("Database not initialized. Some features may be limited.");
|
||||
|
|
@ -267,6 +270,7 @@ export class DBOperations {
|
|||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
private async createNewDb(embeddingInstance: Embeddings | undefined): Promise<Orama<any>> {
|
||||
if (!embeddingInstance) {
|
||||
throw new CustomError("Embedding instance not found.");
|
||||
|
|
@ -298,17 +302,19 @@ export class DBOperations {
|
|||
return db;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public static async getDocsByPath(db: Orama<any>, path: string) {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
if (!path) return;
|
||||
// Use getAllDocuments + JS filter for reliable path matching (handles Unicode/Chinese correctly)
|
||||
const allDocs = await DBOperations.getAllDocuments(db);
|
||||
const filtered = allDocs.filter((doc: any) => doc.path === path);
|
||||
const filtered = allDocs.filter((doc) => (doc as { path?: string }).path === path);
|
||||
// Return in same format as before (hits with document and score)
|
||||
return filtered.map((doc: any) => ({ document: doc, score: 1 }));
|
||||
return filtered.map((doc) => ({ document: doc, score: 1 }));
|
||||
}
|
||||
|
||||
public static async getDocsByEmbedding(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
db: Orama<any>,
|
||||
embedding: number[],
|
||||
options: {
|
||||
|
|
@ -326,6 +332,7 @@ export class DBOperations {
|
|||
return result.hits;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public static async getLatestFileMtime(db: Orama<any> | undefined): Promise<number> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
|
||||
|
|
@ -396,14 +403,14 @@ export class DBOperations {
|
|||
try {
|
||||
await insert(db, docToSave as Parameters<typeof insert>[1]);
|
||||
logInfo(
|
||||
`${existingDoc.hits.length > 0 ? "Updated" : "Inserted"} document ${docToSave.id} in partition ${partition}`
|
||||
`${existingDoc.hits.length > 0 ? "Updated" : "Inserted"} document ${String(docToSave.id)} in partition ${partition}`
|
||||
);
|
||||
|
||||
this.markUnsavedChanges();
|
||||
return docToSave;
|
||||
} catch (insertErr) {
|
||||
logError(
|
||||
`Failed to ${existingDoc.hits.length > 0 ? "update" : "insert"} document ${docToSave.id}:`,
|
||||
`Failed to ${existingDoc.hits.length > 0 ? "update" : "insert"} document ${String(docToSave.id)}:`,
|
||||
insertErr
|
||||
);
|
||||
// If we removed an existing document but failed to insert the new one,
|
||||
|
|
@ -418,7 +425,7 @@ export class DBOperations {
|
|||
return undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
logError(`Error upserting document ${docToSave.id}:`, err);
|
||||
logError(`Error upserting document ${String(docToSave.id)}:`, err);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
|
@ -529,13 +536,14 @@ export class DBOperations {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static async getAllDocuments(db: Orama<any>): Promise<any[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public static async getAllDocuments(db: Orama<any>): Promise<OramaDocument[]> {
|
||||
const result = await search(db, {
|
||||
term: "",
|
||||
limit: 100000,
|
||||
includeVectors: true,
|
||||
});
|
||||
return result.hits.map((hit) => hit.document);
|
||||
return result.hits.map((hit) => hit.document as unknown as OramaDocument);
|
||||
}
|
||||
|
||||
public async garbageCollect(): Promise<number> {
|
||||
|
|
@ -674,28 +682,17 @@ export class DBOperations {
|
|||
});
|
||||
}
|
||||
|
||||
async getDocsJsonByPaths(paths: string[]): Promise<Record<string, any[]>> {
|
||||
async getDocsJsonByPaths(paths: string[]): Promise<Record<string, OramaDocument[]>> {
|
||||
if (!this.oramaDb) {
|
||||
throw new CustomError("Semantic index database not found.");
|
||||
}
|
||||
|
||||
const result: Record<string, any[]> = {};
|
||||
const result: Record<string, OramaDocument[]> = {};
|
||||
|
||||
for (const path of paths) {
|
||||
const docs = await DBOperations.getDocsByPath(this.oramaDb, path);
|
||||
if (docs && docs.length > 0) {
|
||||
result[path] = docs.map((hit) => ({
|
||||
id: hit.document.id,
|
||||
title: hit.document.title,
|
||||
path: hit.document.path,
|
||||
content: hit.document.content,
|
||||
metadata: hit.document.metadata,
|
||||
embedding: hit.document.embedding,
|
||||
embeddingModel: hit.document.embeddingModel,
|
||||
tags: hit.document.tags,
|
||||
extension: hit.document.extension,
|
||||
nchars: hit.document.nchars,
|
||||
}));
|
||||
result[path] = docs.map((hit) => hit.document);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ function shouldUseMiyoForRelevantNotes(): boolean {
|
|||
* @param currentFilePath - The current file path.
|
||||
* @returns A map of the highest score hits for each note.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- InternalTypedDocument<any> is required as Orama controls this type
|
||||
function getHighestScoreHits(hits: Result<InternalTypedDocument<any>>[], currentFilePath: string) {
|
||||
const hitMap = new Map<string, number>();
|
||||
for (const hit of hits) {
|
||||
|
|
@ -103,6 +104,7 @@ async function calculateSimilarityScoreFromOrama({
|
|||
filePath,
|
||||
currentNoteEmbeddings,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
db: Orama<any>;
|
||||
filePath: string;
|
||||
currentNoteEmbeddings: number[][];
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export class HybridRetriever extends BaseRetriever {
|
|||
|
||||
const db = await VectorStoreManager.getInstance().getDb();
|
||||
|
||||
const searchParams: any = {
|
||||
const searchParams: Record<string, unknown> = {
|
||||
similarity: this.options.minSimilarityScore,
|
||||
limit: this.options.maxK,
|
||||
includeVectors: true,
|
||||
|
|
@ -239,7 +239,6 @@ export class HybridRetriever extends BaseRetriever {
|
|||
mtime: { between: [startTime, endTime] },
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const timeIntervalResults = await search(db, searchParams);
|
||||
|
||||
// Convert timeIntervalResults to Document objects
|
||||
|
|
@ -277,7 +276,6 @@ export class HybridRetriever extends BaseRetriever {
|
|||
|
||||
logInfo("Orama search params:\n", searchParams);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const searchResults = await search(db, searchParams);
|
||||
|
||||
// Add null check and validation for search results
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class OramaIndexBackend implements SemanticIndexBackend {
|
|||
if (!hits) {
|
||||
return [];
|
||||
}
|
||||
return hits.map((hit) => hit.document as SemanticIndexDocument);
|
||||
return hits.map((hit) => hit.document);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,6 +197,7 @@ export class OramaIndexBackend implements SemanticIndexBackend {
|
|||
/**
|
||||
* Return the underlying Orama database instance when available.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public getDb(): Orama<any> | undefined {
|
||||
return this.dbOps.getDb();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export class IndexOperations {
|
|||
// Skip documents with invalid embeddings
|
||||
if (!embedding || !Array.isArray(embedding) || embedding.length === 0) {
|
||||
logError(
|
||||
`Invalid embedding for document ${chunk.fileInfo.path}: ${JSON.stringify(embedding)}`
|
||||
`Invalid embedding for document ${String(chunk.fileInfo.path)}: ${JSON.stringify(embedding)}`
|
||||
);
|
||||
this.indexBackend.markFileMissingEmbeddings(chunk.fileInfo.path as string);
|
||||
continue;
|
||||
|
|
@ -196,28 +196,28 @@ export class IndexOperations {
|
|||
|
||||
docsToUpsert.push({
|
||||
doc: {
|
||||
...(chunk.fileInfo as SemanticIndexDocument),
|
||||
...(chunk.fileInfo as unknown as SemanticIndexDocument),
|
||||
id: this.getDocHash(chunk.content),
|
||||
content: chunk.content,
|
||||
embedding,
|
||||
created_at: Date.now(),
|
||||
nchars: chunk.content.length,
|
||||
},
|
||||
filePath: chunk.fileInfo.path,
|
||||
filePath: chunk.fileInfo.path as string,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const chunk of batch) {
|
||||
docsToUpsert.push({
|
||||
doc: {
|
||||
...(chunk.fileInfo as SemanticIndexDocument),
|
||||
...(chunk.fileInfo as unknown as SemanticIndexDocument),
|
||||
id: this.getDocHash(chunk.content),
|
||||
content: chunk.content,
|
||||
embedding: [],
|
||||
created_at: Date.now(),
|
||||
nchars: chunk.content.length,
|
||||
},
|
||||
filePath: chunk.fileInfo.path,
|
||||
filePath: chunk.fileInfo.path as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -262,7 +262,7 @@ export class IndexOperations {
|
|||
}
|
||||
} catch (err) {
|
||||
this.handleError(err, {
|
||||
filePath: batch?.[0]?.fileInfo?.path,
|
||||
filePath: batch?.[0]?.fileInfo?.path as string | undefined,
|
||||
errors,
|
||||
batch,
|
||||
});
|
||||
|
|
@ -320,10 +320,14 @@ export class IndexOperations {
|
|||
Array<{
|
||||
content: string;
|
||||
chunkId: string;
|
||||
fileInfo: any;
|
||||
fileInfo: Record<string, unknown>;
|
||||
}>
|
||||
> {
|
||||
const allChunks: Array<{ content: string; chunkId: string; fileInfo: any }> = [];
|
||||
const allChunks: Array<{
|
||||
content: string;
|
||||
chunkId: string;
|
||||
fileInfo: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
// Process files in batches to bypass ChunkManager's 1000-file limit
|
||||
// The limit exists to protect memory during search queries, but indexing needs all files
|
||||
|
|
@ -486,7 +490,7 @@ export class IndexOperations {
|
|||
}
|
||||
}
|
||||
|
||||
private isStringLengthError(error: any): boolean {
|
||||
private isStringLengthError(error: unknown): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
// Check if it's a direct RangeError
|
||||
|
|
@ -495,17 +499,22 @@ export class IndexOperations {
|
|||
}
|
||||
|
||||
// Check the error message at any depth
|
||||
const message: string = (error.message || error.toString()) as string;
|
||||
const message: string =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: (JSON.stringify(error) ?? "");
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return lowerMessage.includes("string length") || lowerMessage.includes("rangeerror");
|
||||
}
|
||||
|
||||
private handleError(
|
||||
error: any,
|
||||
error: unknown,
|
||||
context?: {
|
||||
filePath?: string;
|
||||
errors?: string[];
|
||||
batch?: Array<{ content: string; fileInfo: any }>;
|
||||
batch?: Array<{ content: string; fileInfo: Record<string, unknown> }>;
|
||||
}
|
||||
): void {
|
||||
const filePath = context?.filePath;
|
||||
|
|
@ -524,8 +533,8 @@ export class IndexOperations {
|
|||
hasFileInfo: !!context.batch[0].fileInfo,
|
||||
}
|
||||
: "No chunks in batch",
|
||||
errorType: error?.constructor?.name,
|
||||
errorMessage: error?.message,
|
||||
errorType: error instanceof Error ? error.constructor?.name : typeof error,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} else {
|
||||
console.error(`Error indexing file ${filePath}:`, error);
|
||||
|
|
@ -548,8 +557,8 @@ export class IndexOperations {
|
|||
// as they spam the user on every file event when the DB is not yet loaded.
|
||||
}
|
||||
|
||||
private isRateLimitError(err: any): boolean {
|
||||
return (err?.message?.includes?.("rate limit") as boolean) || false;
|
||||
private isRateLimitError(err: unknown): boolean {
|
||||
return (err instanceof Error && err.message.toLowerCase().includes("rate limit")) || false;
|
||||
}
|
||||
|
||||
private finalizeIndexing(errors: string[]): void {
|
||||
|
|
@ -613,7 +622,7 @@ export class IndexOperations {
|
|||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
await this.indexBackend.upsert({
|
||||
...(chunk.fileInfo as SemanticIndexDocument),
|
||||
...(chunk.fileInfo as unknown as SemanticIndexDocument),
|
||||
id: this.getDocHash(chunk.content),
|
||||
content: chunk.content,
|
||||
embedding: embeddings[i],
|
||||
|
|
@ -624,7 +633,7 @@ export class IndexOperations {
|
|||
} else {
|
||||
for (const chunk of chunks) {
|
||||
await this.indexBackend.upsert({
|
||||
...(chunk.fileInfo as SemanticIndexDocument),
|
||||
...(chunk.fileInfo as unknown as SemanticIndexDocument),
|
||||
id: this.getDocHash(chunk.content),
|
||||
content: chunk.content,
|
||||
embedding: [],
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export class MergedSemanticRetriever extends BaseRetriever {
|
|||
* @returns Decorated Document instance
|
||||
*/
|
||||
private decorateDocument(doc: Document, source: SourceKind): Document {
|
||||
const metadata: Record<string, any> = {
|
||||
const metadata: Record<string, unknown> = {
|
||||
...(doc.metadata ?? {}),
|
||||
source,
|
||||
};
|
||||
|
|
@ -203,7 +203,7 @@ export class MergedSemanticRetriever extends BaseRetriever {
|
|||
* @param metadata - Document metadata bag
|
||||
* @returns Numeric score or zero when unavailable
|
||||
*/
|
||||
private extractBaseScore(metadata: Record<string, any>): number {
|
||||
private extractBaseScore(metadata: Record<string, unknown>): number {
|
||||
const candidates = [metadata?.rerank_score, metadata?.score];
|
||||
for (const value of candidates) {
|
||||
if (typeof value === "number" && !Number.isNaN(value)) {
|
||||
|
|
@ -227,7 +227,7 @@ export class MergedSemanticRetriever extends BaseRetriever {
|
|||
* @param metadata - Document metadata bag
|
||||
* @returns True if tag matches were present in the explanation
|
||||
*/
|
||||
private hasTagMatch(metadata: Record<string, any>): boolean {
|
||||
private hasTagMatch(metadata: Record<string, unknown>): boolean {
|
||||
const explanation = metadata?.explanation as { lexicalMatches?: unknown } | undefined;
|
||||
if (!explanation) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ Format:
|
|||
this.config.timeout,
|
||||
"Query expansion"
|
||||
);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof TimeoutError) {
|
||||
logInfo(`QueryExpander: Timeout reached for "${query}"`);
|
||||
return this.fallbackExpansion(query);
|
||||
|
|
@ -189,12 +189,18 @@ Format:
|
|||
* @param response - The LLM response object or string
|
||||
* @returns The extracted text content or null if empty
|
||||
*/
|
||||
private extractContent(response: any): string | null {
|
||||
private extractContent(response: unknown): string | null {
|
||||
// Elegant extraction with nullish coalescing
|
||||
const typed = response as { content?: unknown; text?: unknown } | null | undefined;
|
||||
const extracted = typed?.content ?? typed?.text ?? "";
|
||||
return typeof response === "string"
|
||||
? response
|
||||
: String((typed?.content ?? typed?.text ?? "") as any).trim() || null;
|
||||
: (typeof extracted === "string"
|
||||
? extracted
|
||||
: typeof extracted === "number"
|
||||
? String(extracted)
|
||||
: ""
|
||||
).trim() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ jest.mock("./chunks", () => {
|
|||
import { SearchCore, selectDiverseTopK } from "./SearchCore";
|
||||
|
||||
describe("SearchCore retrieve", () => {
|
||||
let mockApp: any;
|
||||
let mockApp: unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
buildFromCandidatesMock.mockReset();
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export class TieredLexicalRetriever extends BaseRetriever {
|
|||
* Supports both chunk IDs (note_path#chunk_index) and note IDs.
|
||||
*/
|
||||
private async convertToDocuments(
|
||||
searchResults: Array<{ id: string; score: number; engine?: string; explanation?: any }>
|
||||
searchResults: Array<{ id: string; score: number; engine?: string; explanation?: unknown }>
|
||||
): Promise<Document[]> {
|
||||
const documents: Document[] = [];
|
||||
|
||||
|
|
@ -155,7 +155,10 @@ export class TieredLexicalRetriever extends BaseRetriever {
|
|||
// Get chunk content (not full note content)
|
||||
// Prefer async getter to auto-regenerate on cache miss; fall back to sync for test mocks
|
||||
let chunkContent = "";
|
||||
const cm: any = this.chunkManager as any;
|
||||
const cm = this.chunkManager as unknown as {
|
||||
getChunkText?: (id: string) => Promise<string>;
|
||||
getChunkTextSync?: (id: string) => string | undefined;
|
||||
};
|
||||
if (typeof cm.getChunkText === "function") {
|
||||
chunkContent = await cm.getChunkText(result.id);
|
||||
} else if (typeof cm.getChunkTextSync === "function") {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jest.mock("obsidian", () => {
|
|||
}
|
||||
|
||||
// Add instanceof compatibility
|
||||
static [Symbol.hasInstance](instance: any): boolean {
|
||||
static [Symbol.hasInstance](instance: unknown): boolean {
|
||||
return Boolean(
|
||||
instance && typeof instance === "object" && "path" in instance && "basename" in instance
|
||||
);
|
||||
|
|
@ -43,9 +43,19 @@ type ChunkManagerInternal = {
|
|||
const asChunkInternal = (m: ChunkManager): ChunkManagerInternal =>
|
||||
m as unknown as ChunkManagerInternal;
|
||||
|
||||
interface MockApp {
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.Mock;
|
||||
cachedRead: jest.Mock;
|
||||
};
|
||||
metadataCache: {
|
||||
getFileCache: jest.Mock;
|
||||
};
|
||||
}
|
||||
|
||||
describe("ChunkManager", () => {
|
||||
let chunkManager: ChunkManager;
|
||||
let mockApp: any;
|
||||
let mockApp: MockApp;
|
||||
// Cache mock files to ensure consistent mtime across calls
|
||||
let mockFileCache: Map<string, TFile>;
|
||||
|
||||
|
|
@ -150,7 +160,7 @@ describe("ChunkManager", () => {
|
|||
},
|
||||
};
|
||||
|
||||
chunkManager = new ChunkManager(mockApp as App);
|
||||
chunkManager = new ChunkManager(mockApp as unknown as App);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -498,7 +508,7 @@ describe("ChunkManager", () => {
|
|||
describe("cache behavior", () => {
|
||||
it("should evict cache when memory limit is exceeded", async () => {
|
||||
// Mock a manager with very small cache limit
|
||||
const smallCacheManager = new ChunkManager(mockApp as App);
|
||||
const smallCacheManager = new ChunkManager(mockApp as unknown as App);
|
||||
asChunkInternal(smallCacheManager).maxCacheBytes = 1000; // 1KB limit
|
||||
|
||||
// Add multiple large documents
|
||||
|
|
@ -724,7 +734,7 @@ describe("ChunkManager", () => {
|
|||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const manager = new ChunkManager(mockApp as App);
|
||||
const manager = new ChunkManager(mockApp as unknown as App);
|
||||
const generatedId = asChunkInternal(manager).generateChunkId("test.md", testCase.index);
|
||||
expect(generatedId).toBe(`test.md#${testCase.expected}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,9 +232,21 @@ type FullTextEngineInternal = {
|
|||
const asInternal = (e: FullTextEngine): FullTextEngineInternal =>
|
||||
e as unknown as FullTextEngineInternal;
|
||||
|
||||
interface MockApp {
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.Mock;
|
||||
cachedRead: jest.Mock;
|
||||
};
|
||||
metadataCache: {
|
||||
getFileCache: jest.Mock;
|
||||
resolvedLinks: Record<string, Record<string, number>>;
|
||||
getBacklinksForFile: jest.Mock;
|
||||
};
|
||||
}
|
||||
|
||||
describe("FullTextEngine", () => {
|
||||
let engine: FullTextEngine;
|
||||
let mockApp: any;
|
||||
let mockApp: MockApp;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock metadata cache
|
||||
|
|
@ -350,7 +362,7 @@ describe("FullTextEngine", () => {
|
|||
},
|
||||
};
|
||||
|
||||
engine = new FullTextEngine(mockApp as App);
|
||||
engine = new FullTextEngine(mockApp as unknown as App);
|
||||
});
|
||||
|
||||
describe("tokenizeMixed", () => {
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ export default class VectorStoreManager {
|
|||
this.indexBackend.onunload();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
|
||||
public async getDb(): Promise<Orama<any>> {
|
||||
await this.waitForInitialization();
|
||||
const db = this.oramaBackend.getDb();
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export interface GroqModel {
|
|||
owned_by: string;
|
||||
active?: boolean;
|
||||
context_window?: number;
|
||||
public_apps?: any;
|
||||
public_apps?: unknown;
|
||||
}
|
||||
|
||||
// XAI response model definition
|
||||
|
|
@ -515,30 +515,44 @@ export const providerAdapters: ProviderModelAdapters = {
|
|||
* Default model adapter - handles unknown provider or format model data
|
||||
* Attempts to detect common data structure patterns and extract relevant information
|
||||
*/
|
||||
/** Coerce an unknown model field value to a string, falling back to a default. */
|
||||
const toStr = (val: unknown, fallback: string): string =>
|
||||
typeof val === "string" && val.length > 0
|
||||
? val
|
||||
: typeof val === "number"
|
||||
? String(val)
|
||||
: fallback;
|
||||
|
||||
export const getDefaultModelAdapter = (provider: SettingKeyProviders) => {
|
||||
return (data: any): StandardModel[] => {
|
||||
return (data: Record<string, unknown>): StandardModel[] => {
|
||||
// Try to detect common data structure patterns
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
return (data.data as any[]).map(
|
||||
(model: any): StandardModel => ({
|
||||
id: model.id || model.name || String(Math.random()),
|
||||
name: model.name || model.id || model.display_name || "Unknown Model",
|
||||
return (data.data as Record<string, unknown>[]).map(
|
||||
(model): StandardModel => ({
|
||||
id: toStr(model.id, "") || toStr(model.name, String(Math.random())),
|
||||
name:
|
||||
toStr(model.name, "") ||
|
||||
toStr(model.id, "") ||
|
||||
toStr(model.display_name, "Unknown Model"),
|
||||
provider: provider,
|
||||
})
|
||||
);
|
||||
} else if (data.models && Array.isArray(data.models)) {
|
||||
return (data.models as any[]).map(
|
||||
(model: any): StandardModel => ({
|
||||
id: model.id || model.name || String(Math.random()),
|
||||
name: model.name || model.displayName || model.id || "Unknown Model",
|
||||
return (data.models as Record<string, unknown>[]).map(
|
||||
(model): StandardModel => ({
|
||||
id: toStr(model.id, "") || toStr(model.name, String(Math.random())),
|
||||
name:
|
||||
toStr(model.name, "") ||
|
||||
toStr(model.displayName, "") ||
|
||||
toStr(model.id, "Unknown Model"),
|
||||
provider: provider,
|
||||
})
|
||||
);
|
||||
} else if (Array.isArray(data)) {
|
||||
return data.map(
|
||||
(model: any): StandardModel => ({
|
||||
id: model.id || model.name || String(Math.random()),
|
||||
name: model.name || model.id || "Unknown Model",
|
||||
return (data as Record<string, unknown>[]).map(
|
||||
(model): StandardModel => ({
|
||||
id: toStr(model.id, "") || toStr(model.name, String(Math.random())),
|
||||
name: toStr(model.name, "") || toStr(model.id, "Unknown Model"),
|
||||
provider: provider,
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const ModelEditModalContent: React.FC<ModelEditModalContentProps> = ({
|
|||
|
||||
// Function to update local state immediately
|
||||
const handleLocalUpdate = useCallback(
|
||||
(field: keyof CustomModel, value: any) => {
|
||||
(field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => {
|
||||
setLocalModel((prevModel) => {
|
||||
const updatedModel = {
|
||||
...prevModel,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ChainType } from "@/chainType";
|
||||
import { logInfo } from "@/logger";
|
||||
import { ChatManager } from "@/core/ChatManager";
|
||||
import { MessageContent } from "@/imageProcessing/imageProcessor";
|
||||
import { ChatMessage, MessageContext } from "@/types/message";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ export class ChatUIState {
|
|||
chainType: ChainType,
|
||||
includeActiveNote: boolean = false,
|
||||
includeActiveWebTab: boolean = false,
|
||||
content?: any[],
|
||||
content?: MessageContent[],
|
||||
updateLoadingMessage?: (message: string) => void
|
||||
): Promise<string> {
|
||||
const messageId = await this.chatManager.sendMessage(
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe("SystemPromptManager", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// Reset the singleton instance before each test
|
||||
(SystemPromptManager as any).instance = undefined;
|
||||
(SystemPromptManager as unknown as Record<string, unknown>).instance = undefined;
|
||||
|
||||
// Clear all mocks
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -92,7 +92,7 @@ describe("SystemPromptManager", () => {
|
|||
});
|
||||
|
||||
it("throws error if vault not provided on first call", () => {
|
||||
(SystemPromptManager as any).instance = undefined;
|
||||
(SystemPromptManager as unknown as Record<string, unknown>).instance = undefined;
|
||||
expect(() => SystemPromptManager.getInstance()).toThrow(
|
||||
"Vault is required for first initialization"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ describe("SystemPromptRegister", () => {
|
|||
let mockVault: Vault;
|
||||
let register: SystemPromptRegister;
|
||||
|
||||
let vaultEventHandlers: Record<string, (...args: any[]) => unknown>;
|
||||
let vaultEventHandlers: Record<string, (...args: unknown[]) => unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -79,7 +79,7 @@ describe("SystemPromptRegister", () => {
|
|||
|
||||
mockPlugin = {} as Plugin;
|
||||
mockVault = {
|
||||
on: jest.fn((event: string, handler: (...args: any[]) => unknown) => {
|
||||
on: jest.fn((event: string, handler: (...args: unknown[]) => unknown) => {
|
||||
vaultEventHandlers[event] = handler;
|
||||
}),
|
||||
off: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -384,18 +384,19 @@ export class Docs4LLMParser implements FileParser {
|
|||
content = markdownParts.join("\n\n");
|
||||
} else if (typeof docs4llmResponse.response === "object") {
|
||||
// Handle single object response (backward compatibility)
|
||||
if (docs4llmResponse.response.md) {
|
||||
content = docs4llmResponse.response.md;
|
||||
} else if (docs4llmResponse.response.text) {
|
||||
content = docs4llmResponse.response.text;
|
||||
} else if (docs4llmResponse.response.content) {
|
||||
content = docs4llmResponse.response.content;
|
||||
const resp = docs4llmResponse.response as Record<string, unknown>;
|
||||
if (resp.md) {
|
||||
content = resp.md as string;
|
||||
} else if (resp.text) {
|
||||
content = resp.text as string;
|
||||
} else if (resp.content) {
|
||||
content = resp.content as string;
|
||||
} else {
|
||||
// If no markdown/text/content field, stringify the entire response
|
||||
content = JSON.stringify(docs4llmResponse.response, null, 2);
|
||||
}
|
||||
} else {
|
||||
content = String(docs4llmResponse.response);
|
||||
content = JSON.stringify(docs4llmResponse.response);
|
||||
}
|
||||
|
||||
// Cache the converted content
|
||||
|
|
@ -421,7 +422,7 @@ export class Docs4LLMParser implements FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
private showRateLimitNotice(error: any): void {
|
||||
private showRateLimitNotice(error: unknown): void {
|
||||
const now = Date.now();
|
||||
|
||||
// Only show one rate limit notice per minute to avoid spam
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as searchUtils from "@/search/searchUtils";
|
||||
import { mockTFile, mockTFolder } from "@/__tests__/mockObsidian";
|
||||
import { ToolManager } from "@/tools/toolManager";
|
||||
import { TFolder } from "obsidian";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import { buildFileTree, createGetFileTreeTool } from "./FileTreeTools";
|
||||
|
||||
// Mock the searchUtils functions
|
||||
|
|
@ -9,82 +10,66 @@ jest.mock("@/search/searchUtils", () => ({
|
|||
shouldIndexFile: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock TFile class
|
||||
class MockTFile {
|
||||
vault: any;
|
||||
stat: { ctime: number; mtime: number; size: number };
|
||||
basename: string;
|
||||
extension: string;
|
||||
name: string;
|
||||
parent: TFolder;
|
||||
path: string;
|
||||
|
||||
constructor(path: string, parent: TFolder) {
|
||||
this.path = path;
|
||||
this.name = path.split("/").pop() || "";
|
||||
this.basename = this.name.split(".")[0];
|
||||
this.extension = this.name.split(".")[1] || "";
|
||||
this.parent = parent;
|
||||
this.vault = {};
|
||||
this.stat = {
|
||||
ctime: Date.now(),
|
||||
mtime: Date.now(),
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Build a TFolder with the given path, parent, and children.
|
||||
*/
|
||||
function makeFolder(path: string, parent: TFolder | null, children: (TFile | TFolder)[]): TFolder {
|
||||
const folder = mockTFolder({ path, name: path.split("/").pop() ?? "", parent, children });
|
||||
return folder;
|
||||
}
|
||||
|
||||
// Mock TFolder class
|
||||
class MockTFolder {
|
||||
vault: any;
|
||||
name: string;
|
||||
path: string;
|
||||
parent: TFolder | null;
|
||||
children: Array<MockTFile | MockTFolder>;
|
||||
|
||||
constructor(path: string, parent: TFolder | null) {
|
||||
this.path = path;
|
||||
this.name = path.split("/").pop() || "";
|
||||
this.parent = parent;
|
||||
this.vault = {};
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
isRoot(): boolean {
|
||||
return this.parent === null;
|
||||
}
|
||||
/**
|
||||
* Build a TFile with the given path and parent.
|
||||
*/
|
||||
function makeFile(path: string, parent: TFolder): TFile {
|
||||
const name = path.split("/").pop() ?? "";
|
||||
const basename = name.includes(".") ? name.split(".")[0] : name;
|
||||
const extension = name.includes(".") ? (name.split(".")[1] ?? "") : "";
|
||||
return mockTFile({
|
||||
path,
|
||||
name,
|
||||
basename,
|
||||
extension,
|
||||
parent,
|
||||
stat: { ctime: Date.now(), mtime: Date.now(), size: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
describe("FileTreeTools", () => {
|
||||
let root: MockTFolder;
|
||||
let root: TFolder;
|
||||
|
||||
beforeEach(() => {
|
||||
root = new MockTFolder("", null);
|
||||
// We need to build the tree bottom-up, then attach children.
|
||||
// Use Object.assign after creation to set children (mockTFolder returns a writable object).
|
||||
root = mockTFolder({ path: "", name: "", parent: null, children: [] });
|
||||
|
||||
// Create a mock file structure
|
||||
const docs = new MockTFolder("docs", root);
|
||||
const projects = new MockTFolder("docs/projects", docs);
|
||||
const notes = new MockTFolder("docs/notes", docs);
|
||||
const docs = makeFolder("docs", root, []);
|
||||
const projects = makeFolder("docs/projects", docs, []);
|
||||
const notes = makeFolder("docs/notes", docs, []);
|
||||
|
||||
docs.children = [projects, notes, new MockTFile("docs/readme.md", docs)];
|
||||
|
||||
projects.children = [
|
||||
new MockTFile("docs/projects/project1.md", projects),
|
||||
new MockTFile("docs/projects/project2.md", projects),
|
||||
new MockTFile("docs/projects/data.json", projects),
|
||||
(projects as TFolder & { children: (TFile | TFolder)[] }).children = [
|
||||
makeFile("docs/projects/project1.md", projects),
|
||||
makeFile("docs/projects/project2.md", projects),
|
||||
makeFile("docs/projects/data.json", projects),
|
||||
];
|
||||
|
||||
notes.children = [
|
||||
new MockTFile("docs/notes/note1.md", notes),
|
||||
new MockTFile("docs/notes/note2.md", notes),
|
||||
new MockTFile("docs/notes/image.png", notes),
|
||||
(notes as TFolder & { children: (TFile | TFolder)[] }).children = [
|
||||
makeFile("docs/notes/note1.md", notes),
|
||||
makeFile("docs/notes/note2.md", notes),
|
||||
makeFile("docs/notes/image.png", notes),
|
||||
];
|
||||
|
||||
root.children = [
|
||||
(docs as TFolder & { children: (TFile | TFolder)[] }).children = [
|
||||
projects,
|
||||
notes,
|
||||
makeFile("docs/readme.md", docs),
|
||||
];
|
||||
|
||||
(root as TFolder & { children: (TFile | TFolder)[] }).children = [
|
||||
docs,
|
||||
new MockTFile("readme.md", root),
|
||||
new MockTFile("config.json", root),
|
||||
new MockTFile("text", root),
|
||||
makeFile("readme.md", root),
|
||||
makeFile("config.json", root),
|
||||
makeFile("text", root),
|
||||
];
|
||||
|
||||
// Reset mocks before each test
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ interface FileTreeNode {
|
|||
extensionCounts?: Record<string, number>;
|
||||
}
|
||||
|
||||
function isTFolder(item: any): item is TFolder {
|
||||
return "children" in item && "path" in item;
|
||||
function isTFolder(item: unknown): item is TFolder {
|
||||
return typeof item === "object" && item !== null && "children" in item && "path" in item;
|
||||
}
|
||||
|
||||
function isTFile(item: any): item is TFile {
|
||||
return "path" in item && !("children" in item);
|
||||
function isTFile(item: unknown): item is TFile {
|
||||
return typeof item === "object" && item !== null && "path" in item && !("children" in item);
|
||||
}
|
||||
|
||||
function getFileExtension(filename: string): string {
|
||||
|
|
|
|||
|
|
@ -209,8 +209,8 @@ async function performLexicalSearch({
|
|||
source: doc.metadata.source,
|
||||
mtime: doc.metadata.mtime ?? null,
|
||||
ctime: doc.metadata.ctime ?? null,
|
||||
chunkId: (doc.metadata as any).chunkId ?? null,
|
||||
isChunk: (doc.metadata as any).isChunk ?? false,
|
||||
chunkId: (doc.metadata as Record<string, unknown>).chunkId ?? null,
|
||||
isChunk: (doc.metadata as Record<string, unknown>).isChunk ?? false,
|
||||
explanation: doc.metadata.explanation ?? null,
|
||||
isFilterResult: isFilter,
|
||||
matchType: isFilter ? doc.metadata.source || "filter" : (undefined as string | undefined),
|
||||
|
|
@ -316,8 +316,8 @@ const semanticSearchTool = createLangChainTool({
|
|||
source: doc.metadata.source,
|
||||
mtime: doc.metadata.mtime ?? null,
|
||||
ctime: doc.metadata.ctime ?? null,
|
||||
chunkId: (doc.metadata as any).chunkId ?? null,
|
||||
isChunk: (doc.metadata as any).isChunk ?? false,
|
||||
chunkId: (doc.metadata as Record<string, unknown>).chunkId ?? null,
|
||||
isChunk: (doc.metadata as Record<string, unknown>).isChunk ?? false,
|
||||
explanation: doc.metadata.explanation ?? null,
|
||||
};
|
||||
});
|
||||
|
|
@ -433,8 +433,8 @@ async function performMiyoSearch({
|
|||
source: doc.metadata.source,
|
||||
mtime: doc.metadata.mtime ?? null,
|
||||
ctime: doc.metadata.ctime ?? null,
|
||||
chunkId: (doc.metadata as any).chunkId ?? null,
|
||||
isChunk: (doc.metadata as any).isChunk ?? false,
|
||||
chunkId: (doc.metadata as Record<string, unknown>).chunkId ?? null,
|
||||
isChunk: (doc.metadata as Record<string, unknown>).isChunk ?? false,
|
||||
explanation: doc.metadata.explanation ?? null,
|
||||
isFilterResult: isFilter,
|
||||
matchType: isFilter ? doc.metadata.source || "filter" : (undefined as string | undefined),
|
||||
|
|
@ -523,10 +523,10 @@ const indexTool = createLangChainTool({
|
|||
`Semantic search index has been refreshed with ${count} documents.`,
|
||||
documentCount: count,
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to index with semantic search: ${error.message}`,
|
||||
message: `Failed to index with semantic search: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -40,20 +40,21 @@ function clampReadNoteMessage(message: string): string {
|
|||
return `${trimmed.slice(0, READ_NOTE_SUMMARY_MAX_LENGTH)}…`;
|
||||
}
|
||||
|
||||
function summarizeReadNotePayload(payload: any): string | null {
|
||||
function summarizeReadNotePayload(payload: unknown): string | null {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = typeof payload.status === "string" ? (payload.status as string) : null;
|
||||
const p = payload as Record<string, unknown>;
|
||||
const status = typeof p.status === "string" ? p.status : null;
|
||||
const message =
|
||||
typeof payload.message === "string" && (payload.message as string).trim().length > 0
|
||||
? clampReadNoteMessage(payload.message as string)
|
||||
typeof p.message === "string" && p.message.trim().length > 0
|
||||
? clampReadNoteMessage(p.message)
|
||||
: null;
|
||||
const notePath = typeof payload.notePath === "string" ? (payload.notePath as string) : "";
|
||||
const notePath = typeof p.notePath === "string" ? p.notePath : "";
|
||||
const noteTitle =
|
||||
typeof payload.noteTitle === "string" && (payload.noteTitle as string).trim().length > 0
|
||||
? (payload.noteTitle as string).trim()
|
||||
typeof p.noteTitle === "string" && p.noteTitle.trim().length > 0
|
||||
? p.noteTitle.trim()
|
||||
: deriveReadNoteDisplayName(notePath);
|
||||
const displayName = noteTitle || deriveReadNoteDisplayName(notePath);
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ function summarizeReadNotePayload(payload: any): string | null {
|
|||
return message ?? `⚠️ Note "${displayName}" not found`;
|
||||
}
|
||||
if (status === "not_unique") {
|
||||
const candidateCount = Array.isArray(payload.candidates) ? payload.candidates.length : 0;
|
||||
const candidateCount = Array.isArray(p.candidates) ? p.candidates.length : 0;
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
|
@ -80,13 +81,9 @@ function summarizeReadNotePayload(payload: any): string | null {
|
|||
return message;
|
||||
}
|
||||
const totalChunks =
|
||||
typeof payload.totalChunks === "number" && Number.isFinite(payload.totalChunks)
|
||||
? payload.totalChunks
|
||||
: null;
|
||||
typeof p.totalChunks === "number" && Number.isFinite(p.totalChunks) ? p.totalChunks : null;
|
||||
const requested =
|
||||
typeof payload.chunkIndex === "number" && Number.isFinite(payload.chunkIndex)
|
||||
? payload.chunkIndex
|
||||
: null;
|
||||
typeof p.chunkIndex === "number" && Number.isFinite(p.chunkIndex) ? p.chunkIndex : null;
|
||||
if (requested !== null && totalChunks !== null) {
|
||||
const maxIndex = Math.max(totalChunks - 1, 0);
|
||||
return `⚠️ Chunk ${requested} exceeds available range (max index ${maxIndex})`;
|
||||
|
|
@ -95,14 +92,10 @@ function summarizeReadNotePayload(payload: any): string | null {
|
|||
}
|
||||
|
||||
const chunkIndex =
|
||||
typeof payload.chunkIndex === "number" && Number.isFinite(payload.chunkIndex)
|
||||
? payload.chunkIndex
|
||||
: 0;
|
||||
typeof p.chunkIndex === "number" && Number.isFinite(p.chunkIndex) ? p.chunkIndex : 0;
|
||||
const totalChunks =
|
||||
typeof payload.totalChunks === "number" && Number.isFinite(payload.totalChunks)
|
||||
? payload.totalChunks
|
||||
: null;
|
||||
const hasMore = Boolean(payload.hasMore);
|
||||
typeof p.totalChunks === "number" && Number.isFinite(p.totalChunks) ? p.totalChunks : null;
|
||||
const hasMore = Boolean(p.hasMore);
|
||||
|
||||
const parts: string[] = [`✅ Read "${displayName || "note"}"`];
|
||||
if (totalChunks && totalChunks > 0) {
|
||||
|
|
@ -135,7 +128,7 @@ export class ToolResultFormatter {
|
|||
}
|
||||
|
||||
// Try to parse as JSON for all tools now that they return JSON
|
||||
let parsedResult: any;
|
||||
let parsedResult: unknown;
|
||||
try {
|
||||
parsedResult = JSON.parse(normalized);
|
||||
} catch {
|
||||
|
|
@ -172,17 +165,19 @@ export class ToolResultFormatter {
|
|||
* @param documents Array of parsed local search documents
|
||||
* @returns Display-friendly summary string
|
||||
*/
|
||||
static formatLocalSearchDocuments(documents: any[]): string {
|
||||
static formatLocalSearchDocuments(documents: unknown[]): string {
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
return "📚 Found 0 relevant notes\n\nNo matching notes found.";
|
||||
}
|
||||
|
||||
const total = documents.length;
|
||||
const topResults = documents.slice(0, 10);
|
||||
const hasScoringData = topResults.some(
|
||||
(item) =>
|
||||
const hasScoringData = topResults.some((doc) => {
|
||||
const item = doc as Record<string, unknown>;
|
||||
return (
|
||||
typeof item?.rerank_score === "number" || typeof item?.score === "number" || item?.source
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const formattedItems = topResults
|
||||
.map((item, index) =>
|
||||
|
|
@ -197,7 +192,7 @@ export class ToolResultFormatter {
|
|||
return `📚 Found ${total} relevant notes\n\nTop results:\n\n${formattedItems}${footer}`;
|
||||
}
|
||||
|
||||
private static formatLocalSearch(result: any): string {
|
||||
private static formatLocalSearch(result: unknown): string {
|
||||
// Handle XML-wrapped results from chain runners
|
||||
if (typeof result === "string") {
|
||||
// Check if it's XML-wrapped content
|
||||
|
|
@ -215,7 +210,7 @@ export class ToolResultFormatter {
|
|||
}
|
||||
|
||||
// Robustly extract document information regardless of tag ordering
|
||||
const documents: any[] = [];
|
||||
const documents: unknown[] = [];
|
||||
const blockRegex = /<document>([\s\S]*?)<\/document>/g;
|
||||
let blockMatch;
|
||||
while ((blockMatch = blockRegex.exec(xmlContent)) !== null) {
|
||||
|
|
@ -251,20 +246,26 @@ export class ToolResultFormatter {
|
|||
return this.formatLocalSearchDocuments(searchResults);
|
||||
}
|
||||
|
||||
private static parseSearchResults(result: any): any[] {
|
||||
private static parseSearchResults(result: unknown): unknown[] {
|
||||
// Only support the new structured format or pre-formatted XML flow
|
||||
if (typeof result === "object" && result !== null) {
|
||||
if (result.type === "local_search" && Array.isArray(result.documents)) {
|
||||
return result.documents as any[];
|
||||
const r = result as Record<string, unknown>;
|
||||
if (r.type === "local_search" && Array.isArray(r.documents)) {
|
||||
return r.documents as unknown[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (typeof result === "string") {
|
||||
// Allow parsing of structured JSON string
|
||||
try {
|
||||
const parsed = JSON.parse(result);
|
||||
if (parsed && parsed.type === "local_search" && Array.isArray(parsed.documents)) {
|
||||
return parsed.documents as any[];
|
||||
const parsed = JSON.parse(result) as unknown;
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
(parsed as Record<string, unknown>).type === "local_search" &&
|
||||
Array.isArray((parsed as Record<string, unknown>).documents)
|
||||
) {
|
||||
return (parsed as Record<string, unknown>).documents as unknown[];
|
||||
}
|
||||
} catch {
|
||||
// ignore JSON parse errors; fall through to empty array
|
||||
|
|
@ -274,58 +275,76 @@ export class ToolResultFormatter {
|
|||
return [];
|
||||
}
|
||||
|
||||
private static formatSearchItem(item: any, index: number): string {
|
||||
const filename = item.path?.split("/").pop()?.replace(/\.md$/, "") || item.title || "Untitled";
|
||||
const score = item.rerank_score || item.score || 0;
|
||||
private static formatSearchItem(item: unknown, index: number): string {
|
||||
const it = item as Record<string, unknown>;
|
||||
const pathStr = typeof it.path === "string" ? it.path : "";
|
||||
const titleStr = typeof it.title === "string" ? it.title : "";
|
||||
const filename = pathStr
|
||||
? pathStr.split("/").pop()?.replace(/\.md$/, "") || titleStr || "Untitled"
|
||||
: titleStr || "Untitled";
|
||||
const score = (it.rerank_score as number) || (it.score as number) || 0;
|
||||
const scoreDisplay = typeof score === "number" ? score.toFixed(4) : score;
|
||||
|
||||
// For time-filtered results, show as "Recency" instead of "Relevance"
|
||||
const scoreLabel = item.source === "time-filtered" ? "Recency" : "Relevance";
|
||||
const scoreLabel = it.source === "time-filtered" ? "Recency" : "Relevance";
|
||||
|
||||
const lines = [`${index + 1}. ${filename}`];
|
||||
|
||||
// For time-filtered queries, show actual modified time instead of a recency score
|
||||
if (item.source === "time-filtered") {
|
||||
if (item.mtime) {
|
||||
if (it.source === "time-filtered") {
|
||||
if (it.mtime) {
|
||||
try {
|
||||
const d = new Date(item.mtime as string | number | Date);
|
||||
const iso = isNaN(d.getTime()) ? String(item.mtime) : d.toISOString();
|
||||
lines.push(` 🕒 Modified: ${iso}${item.includeInContext ? " ✓" : ""}`);
|
||||
const mtimeVal = it.mtime as string | number | Date;
|
||||
const d = new Date(mtimeVal);
|
||||
const iso = isNaN(d.getTime())
|
||||
? typeof mtimeVal === "string" || typeof mtimeVal === "number"
|
||||
? String(mtimeVal)
|
||||
: ""
|
||||
: d.toISOString();
|
||||
lines.push(` 🕒 Modified: ${iso}${it.includeInContext ? " ✓" : ""}`);
|
||||
} catch {
|
||||
lines.push(` 🕒 Modified: ${String(item.mtime)}${item.includeInContext ? " ✓" : ""}`);
|
||||
const mtimeStr =
|
||||
typeof it.mtime === "string" || typeof it.mtime === "number" ? String(it.mtime) : "";
|
||||
lines.push(` 🕒 Modified: ${mtimeStr}${it.includeInContext ? " ✓" : ""}`);
|
||||
}
|
||||
}
|
||||
} else if (item.source === "title-match") {
|
||||
} else if (it.source === "title-match") {
|
||||
// For title matches, avoid misleading numeric scores; mark as a title match
|
||||
lines.push(` 🔖 Title match${item.includeInContext ? " ✓" : ""}`);
|
||||
lines.push(` 🔖 Title match${it.includeInContext ? " ✓" : ""}`);
|
||||
} else {
|
||||
// Default: show relevance-like score line
|
||||
lines.push(` 📊 ${scoreLabel}: ${scoreDisplay}${item.includeInContext ? " ✓" : ""}`);
|
||||
lines.push(` 📊 ${scoreLabel}: ${scoreDisplay}${it.includeInContext ? " ✓" : ""}`);
|
||||
}
|
||||
|
||||
const snippet = this.extractContentSnippet(item.content as string);
|
||||
const snippet = this.extractContentSnippet(it.content as string);
|
||||
if (snippet) {
|
||||
lines.push(` 💬 "${snippet}${item.content?.length > 150 ? "..." : ""}"`);
|
||||
const contentLength = typeof it.content === "string" ? it.content.length : 0;
|
||||
lines.push(` 💬 "${snippet}${contentLength > 150 ? "..." : ""}"`);
|
||||
}
|
||||
|
||||
if (item.path && !item.path.endsWith(`/${filename}.md`)) {
|
||||
lines.push(` 📁 ${item.path}`);
|
||||
if (pathStr && !pathStr.endsWith(`/${filename}.md`)) {
|
||||
lines.push(` 📁 ${pathStr}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private static formatBasicSearchItem(item: any, index: number): string {
|
||||
const title = item.title || item.path || `Result ${index + 1}`;
|
||||
private static formatBasicSearchItem(item: unknown, index: number): string {
|
||||
const it = item as Record<string, unknown>;
|
||||
const title = (it.title as string) || (it.path as string) || `Result ${index + 1}`;
|
||||
const lines = [`${index + 1}. ${title}`];
|
||||
|
||||
const modified = item.mtime || item.modified || item.modified_at || item.updated_at;
|
||||
const modified = it.mtime || it.modified || it.modified_at || it.updated_at;
|
||||
if (modified) {
|
||||
lines.push(` 🕒 Modified: ${String(modified)}`);
|
||||
const modifiedStr =
|
||||
typeof modified === "string" || typeof modified === "number" ? String(modified) : "";
|
||||
if (modifiedStr) {
|
||||
lines.push(` 🕒 Modified: ${modifiedStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.path && item.path !== title) {
|
||||
lines.push(` 📁 ${item.path}`);
|
||||
if (typeof it.path === "string" && it.path && it.path !== title) {
|
||||
lines.push(` 📁 ${it.path}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
|
|
@ -341,31 +360,35 @@ export class ToolResultFormatter {
|
|||
return cleanContent.substring(0, maxLength).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
private static formatWebSearch(result: any): string {
|
||||
private static formatWebSearch(result: unknown): string {
|
||||
// Handle new JSON array format from webSearch tool
|
||||
if (Array.isArray(result) && result.length > 0 && result[0].type === "web_search") {
|
||||
const firstItem =
|
||||
Array.isArray(result) && result.length > 0 ? (result[0] as Record<string, unknown>) : null;
|
||||
if (firstItem && firstItem.type === "web_search") {
|
||||
const output: string[] = ["🌐 Web Search Results"];
|
||||
const item = result[0];
|
||||
|
||||
// Add the main content
|
||||
if (item.content) {
|
||||
if (firstItem.content) {
|
||||
output.push("");
|
||||
output.push(item.content as string);
|
||||
output.push(typeof firstItem.content === "string" ? firstItem.content : "");
|
||||
}
|
||||
|
||||
// Add citations if present
|
||||
if (item.citations && item.citations.length > 0) {
|
||||
const citations = Array.isArray(firstItem.citations) ? firstItem.citations : [];
|
||||
if (citations.length > 0) {
|
||||
output.push("");
|
||||
output.push("Sources:");
|
||||
item.citations.forEach((url: string, index: number) => {
|
||||
output.push(`[${index + 1}] ${url}`);
|
||||
citations.forEach((url: unknown, index: number) => {
|
||||
output.push(`[${index + 1}] ${String(url)}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Add instruction for the model
|
||||
if (item.instruction) {
|
||||
if (firstItem.instruction) {
|
||||
output.push("");
|
||||
output.push(`Note: ${item.instruction}`);
|
||||
output.push(
|
||||
`Note: ${typeof firstItem.instruction === "string" ? firstItem.instruction : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
|
|
@ -415,12 +438,12 @@ export class ToolResultFormatter {
|
|||
return output.join("\n");
|
||||
}
|
||||
|
||||
return result as string;
|
||||
return typeof result === "string" ? result : "";
|
||||
}
|
||||
|
||||
private static formatYoutubeTranscription(result: any): string {
|
||||
private static formatYoutubeTranscription(result: unknown): string {
|
||||
// Handle both string and object results
|
||||
let parsed: any;
|
||||
let parsed: unknown;
|
||||
|
||||
if (typeof result === "string") {
|
||||
try {
|
||||
|
|
@ -432,28 +455,42 @@ export class ToolResultFormatter {
|
|||
} else if (typeof result === "object") {
|
||||
parsed = result;
|
||||
} else {
|
||||
return String(result);
|
||||
return typeof result === "number" || typeof result === "boolean" ? String(result) : "";
|
||||
}
|
||||
|
||||
// Narrow parsed to a record for member access
|
||||
const p =
|
||||
typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : null;
|
||||
if (!p) {
|
||||
return typeof result === "object" ? JSON.stringify(result, null, 2) : "";
|
||||
}
|
||||
|
||||
// Handle error case
|
||||
if (parsed.success === false) {
|
||||
return `📺 YouTube Transcription Failed\n\n${parsed.message}`;
|
||||
if (p.success === false) {
|
||||
return `📺 YouTube Transcription Failed\n\n${typeof p.message === "string" ? p.message : ""}`;
|
||||
}
|
||||
|
||||
// Handle new multi-URL format
|
||||
if (parsed.results && Array.isArray(parsed.results)) {
|
||||
if (p.results && Array.isArray(p.results)) {
|
||||
const totalUrls = typeof p.total_urls === "number" ? p.total_urls : p.results.length;
|
||||
const output: string[] = [
|
||||
`📺 YouTube Transcripts (${parsed.total_urls} video${parsed.total_urls > 1 ? "s" : ""})`,
|
||||
`📺 YouTube Transcripts (${totalUrls} video${totalUrls > 1 ? "s" : ""})`,
|
||||
];
|
||||
output.push("");
|
||||
|
||||
for (const videoResult of parsed.results) {
|
||||
if (videoResult.success) {
|
||||
output.push(`📹 Video: ${videoResult.url}`);
|
||||
for (const videoResult of p.results) {
|
||||
const vr =
|
||||
typeof videoResult === "object" && videoResult !== null
|
||||
? (videoResult as Record<string, unknown>)
|
||||
: null;
|
||||
if (!vr) continue;
|
||||
if (vr.success) {
|
||||
output.push(`📹 Video: ${typeof vr.url === "string" ? vr.url : ""}`);
|
||||
output.push("");
|
||||
|
||||
// Format transcript
|
||||
const lines = videoResult.transcript.split("\n");
|
||||
const transcript = typeof vr.transcript === "string" ? vr.transcript : "";
|
||||
const lines = transcript.split("\n");
|
||||
let formattedLines = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
|
|
@ -477,13 +514,15 @@ export class ToolResultFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
if (videoResult.elapsed_time_ms) {
|
||||
if (vr.elapsed_time_ms) {
|
||||
output.push("");
|
||||
output.push(`Processing time: ${(videoResult.elapsed_time_ms / 1000).toFixed(1)}s`);
|
||||
output.push(
|
||||
`Processing time: ${(typeof vr.elapsed_time_ms === "number" ? vr.elapsed_time_ms / 1000 : 0).toFixed(1)}s`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
output.push(`❌ Failed to transcribe: ${videoResult.url}`);
|
||||
output.push(` ${videoResult.message}`);
|
||||
output.push(`❌ Failed to transcribe: ${typeof vr.url === "string" ? vr.url : ""}`);
|
||||
output.push(` ${typeof vr.message === "string" ? vr.message : ""}`);
|
||||
}
|
||||
|
||||
output.push("");
|
||||
|
|
@ -495,12 +534,13 @@ export class ToolResultFormatter {
|
|||
}
|
||||
|
||||
// Handle old single-video format
|
||||
if (parsed.transcript) {
|
||||
if (p.transcript) {
|
||||
const output: string[] = ["📺 YouTube Transcript"];
|
||||
output.push("");
|
||||
|
||||
// Split transcript into manageable chunks
|
||||
const lines = parsed.transcript.split("\n");
|
||||
const transcript = typeof p.transcript === "string" ? p.transcript : "";
|
||||
const lines = transcript.split("\n");
|
||||
let formattedLines = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
|
|
@ -524,22 +564,32 @@ export class ToolResultFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
if (parsed.elapsed_time_ms) {
|
||||
if (p.elapsed_time_ms) {
|
||||
output.push("");
|
||||
output.push(`Processing time: ${(parsed.elapsed_time_ms / 1000).toFixed(1)}s`);
|
||||
output.push(
|
||||
`Processing time: ${(typeof p.elapsed_time_ms === "number" ? p.elapsed_time_ms / 1000 : 0).toFixed(1)}s`
|
||||
);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
// If we can't format it, return as string
|
||||
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result);
|
||||
return typeof result === "object" ? JSON.stringify(result, null, 2) : "";
|
||||
}
|
||||
|
||||
private static formatWriteToFile(result: any): string {
|
||||
private static formatWriteToFile(result: unknown): string {
|
||||
// Extract result status from object or use string directly
|
||||
const status = typeof result === "object" ? result.result : result;
|
||||
const statusStr = String(status).toLowerCase();
|
||||
const r =
|
||||
typeof result === "object" && result !== null ? (result as Record<string, unknown>) : null;
|
||||
const status = r ? r.result : result;
|
||||
const statusStr = (
|
||||
typeof status === "string"
|
||||
? status
|
||||
: typeof status === "number" || typeof status === "boolean"
|
||||
? String(status)
|
||||
: ""
|
||||
).toLowerCase();
|
||||
|
||||
if (statusStr.includes("accepted")) {
|
||||
return "✅ File change: accepted";
|
||||
|
|
@ -548,17 +598,29 @@ export class ToolResultFormatter {
|
|||
}
|
||||
|
||||
// Return message if available, otherwise the raw result
|
||||
return typeof result === "object" && result.message
|
||||
? (result.message as string)
|
||||
: String(status);
|
||||
return r && typeof r.message === "string"
|
||||
? r.message
|
||||
: typeof status === "string"
|
||||
? status
|
||||
: "";
|
||||
}
|
||||
|
||||
private static formatReplaceInFile(result: any): string {
|
||||
const status = typeof result === "object" ? String(result.result ?? "") : String(result);
|
||||
const diff = typeof result === "object" ? result.diff : undefined;
|
||||
private static formatReplaceInFile(result: unknown): string {
|
||||
const r =
|
||||
typeof result === "object" && result !== null ? (result as Record<string, unknown>) : null;
|
||||
const rResult = r ? r.result : undefined;
|
||||
const status = r
|
||||
? typeof rResult === "string" || typeof rResult === "number" || typeof rResult === "boolean"
|
||||
? String(rResult)
|
||||
: ""
|
||||
: typeof result === "string"
|
||||
? result
|
||||
: "";
|
||||
const diff = r ? r.diff : undefined;
|
||||
const diffStr = typeof diff === "string" ? diff : typeof diff === "number" ? String(diff) : "";
|
||||
|
||||
if (status.toLowerCase().includes("accepted") && diff) {
|
||||
return `✅ Edit accepted\n\`\`\`diff\n${diff}\n\`\`\``;
|
||||
if (status.toLowerCase().includes("accepted") && diffStr) {
|
||||
return `✅ Edit accepted\n\`\`\`diff\n${diffStr}\n\`\`\``;
|
||||
} else if (status.toLowerCase().includes("accepted")) {
|
||||
return "✅ Edit accepted";
|
||||
} else if (status.toLowerCase().includes("rejected")) {
|
||||
|
|
@ -566,11 +628,11 @@ export class ToolResultFormatter {
|
|||
}
|
||||
|
||||
// Error / not-found strings pass through unchanged
|
||||
return typeof result === "object" && result.message ? (result.message as string) : status;
|
||||
return r && typeof r.message === "string" ? r.message : status;
|
||||
}
|
||||
|
||||
private static formatReadNote(result: any): string {
|
||||
let payload: any = result;
|
||||
private static formatReadNote(result: unknown): string {
|
||||
let payload: unknown = result;
|
||||
if (typeof result === "string") {
|
||||
try {
|
||||
payload = JSON.parse(result);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue