mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Add Miyo document parsing toggle and PDF parse-doc integration (#2199)
* feat: add miyo document parsing toggle for PDF parser * refactor: consolidate miyo toggles into single enable flag
This commit is contained in:
parent
30b54b6fdd
commit
ea552077d9
4 changed files with 221 additions and 4 deletions
93
src/miyo/MiyoClient.test.ts
Normal file
93
src/miyo/MiyoClient.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { MiyoClient } from "@/miyo/MiyoClient";
|
||||
import { MiyoServiceDiscovery } from "@/miyo/MiyoServiceDiscovery";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { requestUrl } from "obsidian";
|
||||
|
||||
jest.mock("obsidian", () => ({
|
||||
requestUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/settings/model", () => ({
|
||||
getSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockResolveBaseUrl = jest.fn();
|
||||
|
||||
jest.mock("@/miyo/MiyoServiceDiscovery", () => ({
|
||||
MiyoServiceDiscovery: {
|
||||
getInstance: jest.fn(() => ({
|
||||
resolveBaseUrl: (...args: unknown[]) => mockResolveBaseUrl(...args),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("@/logger", () => ({
|
||||
logInfo: jest.fn(),
|
||||
logWarn: jest.fn(),
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MiyoClient.parseDoc", () => {
|
||||
const mockedRequestUrl = requestUrl as jest.MockedFunction<typeof requestUrl>;
|
||||
const mockedGetSettings = getSettings as jest.MockedFunction<typeof getSettings>;
|
||||
const mockedGetInstance = MiyoServiceDiscovery.getInstance as unknown as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedGetSettings.mockReturnValue({
|
||||
selfHostApiKey: "",
|
||||
debug: false,
|
||||
} as any);
|
||||
mockResolveBaseUrl.mockResolvedValue("http://127.0.0.1:8742");
|
||||
mockedGetInstance.mockReturnValue({
|
||||
resolveBaseUrl: mockResolveBaseUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it("posts absolute path to /v0/parse-doc and returns parsed payload", async () => {
|
||||
mockedRequestUrl.mockResolvedValue({
|
||||
status: 200,
|
||||
json: {
|
||||
text: "parsed text",
|
||||
format: "pdf",
|
||||
source_path: "/tmp/sample.pdf",
|
||||
title: "Sample",
|
||||
page_count: 3,
|
||||
},
|
||||
text: "",
|
||||
} as any);
|
||||
|
||||
const client = new MiyoClient();
|
||||
const result = await client.parseDoc("http://127.0.0.1:8742", "/tmp/sample.pdf");
|
||||
|
||||
expect(result).toEqual({
|
||||
text: "parsed text",
|
||||
format: "pdf",
|
||||
source_path: "/tmp/sample.pdf",
|
||||
title: "Sample",
|
||||
page_count: 3,
|
||||
});
|
||||
expect(mockedRequestUrl).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "http://127.0.0.1:8742/v0/parse-doc",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ path: "/tmp/sample.pdf" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when /v0/parse-doc returns an error status", async () => {
|
||||
mockedRequestUrl.mockResolvedValue({
|
||||
status: 500,
|
||||
text: "internal server error",
|
||||
json: { detail: "fail" },
|
||||
} as any);
|
||||
|
||||
const client = new MiyoClient();
|
||||
|
||||
await expect(client.parseDoc("http://127.0.0.1:8742", "/tmp/sample.pdf")).rejects.toThrow(
|
||||
"Miyo request failed with status 500"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -103,6 +103,17 @@ export interface MiyoRelatedSearchResponse {
|
|||
results: MiyoRelatedSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for Miyo document parsing endpoint.
|
||||
*/
|
||||
export interface MiyoParseDocResponse {
|
||||
text: string;
|
||||
format: string;
|
||||
source_path: string;
|
||||
title?: string;
|
||||
page_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search filters for Miyo queries.
|
||||
*/
|
||||
|
|
@ -331,6 +342,20 @@ export class MiyoClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a local document via Miyo.
|
||||
*
|
||||
* @param baseUrl - Miyo base URL.
|
||||
* @param path - Absolute local file path.
|
||||
* @returns Parsed document response.
|
||||
*/
|
||||
public async parseDoc(baseUrl: string, path: string): Promise<MiyoParseDocResponse> {
|
||||
return this.requestJson<MiyoParseDocResponse>(baseUrl, "/v0/parse-doc", {
|
||||
method: "POST",
|
||||
body: { path },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request headers, including optional auth.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -214,8 +214,8 @@ export const CopilotPlusSettings: React.FC = () => {
|
|||
<>
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Enable Miyo Search"
|
||||
description="Use Miyo as your local search engine and context hub — supports larger vaults than built-in Copilot search. Enabling this will prompt a one-time index refresh."
|
||||
title="Enable Miyo"
|
||||
description="Use Miyo as your local search, PDF parsing, and context hub. Enabling this will prompt a one-time index refresh."
|
||||
checked={settings.enableMiyoSearch}
|
||||
onCheckedChange={handleMiyoSearchToggle}
|
||||
disabled={isValidatingSelfHost}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
|||
import { ProjectConfig } from "@/aiParams";
|
||||
import { PDFCache } from "@/cache/pdfCache";
|
||||
import { ProjectContextCache } from "@/cache/projectContextCache";
|
||||
import { logError, logInfo } from "@/logger";
|
||||
import { logError, logInfo, logWarn } from "@/logger";
|
||||
import { MiyoClient } from "@/miyo/MiyoClient";
|
||||
import { isSelfHostModeValid } from "@/plusUtils";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { extractRetryTime, isRateLimitError } from "@/utils/rateLimitUtils";
|
||||
import { Notice, TFile, Vault } from "obsidian";
|
||||
import { FileSystemAdapter, Notice, TFile, Vault } from "obsidian";
|
||||
import { CanvasLoader } from "./CanvasLoader";
|
||||
|
||||
interface FileParser {
|
||||
|
|
@ -12,6 +15,81 @@ interface FileParser {
|
|||
parseFile: (file: TFile, vault: Vault) => Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve absolute file path for a vault file when supported by the adapter.
|
||||
*
|
||||
* @param file - Target file.
|
||||
* @param vault - Obsidian vault instance.
|
||||
* @returns Absolute file path or null when unavailable.
|
||||
*/
|
||||
function resolveAbsoluteFilePath(file: TFile, vault: Vault): string | null {
|
||||
const adapter = vault.adapter;
|
||||
if (adapter instanceof FileSystemAdapter) {
|
||||
return adapter.getFullPath(file.path);
|
||||
}
|
||||
|
||||
const adapterAny = adapter as unknown as { getFullPath?: (normalizedPath: string) => string };
|
||||
if (typeof adapterAny.getFullPath === "function") {
|
||||
return adapterAny.getFullPath(file.path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-host PDF parser bridge using Miyo parse-doc endpoint.
|
||||
*/
|
||||
class SelfHostPdfParser {
|
||||
private miyoClient: MiyoClient;
|
||||
|
||||
/**
|
||||
* Create a new self-host PDF parser.
|
||||
*/
|
||||
constructor() {
|
||||
this.miyoClient = new MiyoClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a PDF via Miyo when self-host mode is active.
|
||||
*
|
||||
* @param file - PDF file to parse.
|
||||
* @param vault - Obsidian vault instance.
|
||||
* @returns Parsed text when successful, otherwise null.
|
||||
*/
|
||||
public async parsePdf(file: TFile, vault: Vault): Promise<string | null> {
|
||||
const settings = getSettings();
|
||||
if (!settings.enableMiyoSearch || file.extension.toLowerCase() !== "pdf") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const absolutePath = resolveAbsoluteFilePath(file, vault);
|
||||
if (!absolutePath) {
|
||||
logWarn(
|
||||
`[SelfHostPdfParser] Could not resolve absolute path for ${file.path}; cannot call Miyo parse-doc`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = await this.miyoClient.resolveBaseUrl(settings.selfHostUrl);
|
||||
const response = await this.miyoClient.parseDoc(baseUrl, absolutePath);
|
||||
if (typeof response.text !== "string" || response.text.trim().length === 0) {
|
||||
throw new Error("Miyo parse-doc returned empty text");
|
||||
}
|
||||
|
||||
logInfo(`[SelfHostPdfParser] Parsed PDF via Miyo: ${file.path}`);
|
||||
return response.text;
|
||||
} catch (error) {
|
||||
logWarn(
|
||||
`[SelfHostPdfParser] Failed to parse ${file.path} via Miyo parse-doc: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MarkdownParser implements FileParser {
|
||||
supportedExtensions = ["md"];
|
||||
|
||||
|
|
@ -24,10 +102,12 @@ export class PDFParser implements FileParser {
|
|||
supportedExtensions = ["pdf"];
|
||||
private brevilabsClient: BrevilabsClient;
|
||||
private pdfCache: PDFCache;
|
||||
private selfHostPdfParser: SelfHostPdfParser;
|
||||
|
||||
constructor(brevilabsClient: BrevilabsClient) {
|
||||
this.brevilabsClient = brevilabsClient;
|
||||
this.pdfCache = PDFCache.getInstance();
|
||||
this.selfHostPdfParser = new SelfHostPdfParser();
|
||||
}
|
||||
|
||||
async parseFile(file: TFile, vault: Vault): Promise<string> {
|
||||
|
|
@ -41,6 +121,25 @@ export class PDFParser implements FileParser {
|
|||
return cachedResponse.response;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
if (
|
||||
isSelfHostModeValid() &&
|
||||
settings.enableMiyoSearch &&
|
||||
file.extension.toLowerCase() === "pdf"
|
||||
) {
|
||||
const selfHostPdfContent = await this.selfHostPdfParser.parsePdf(file, vault);
|
||||
if (selfHostPdfContent !== null) {
|
||||
await this.pdfCache.set(file, {
|
||||
response: selfHostPdfContent,
|
||||
elapsed_time_ms: 0,
|
||||
});
|
||||
return selfHostPdfContent;
|
||||
}
|
||||
|
||||
logError(`[PDFParser] Miyo enabled but parse-doc failed for ${file.path}`);
|
||||
return `[Error: Could not extract content from PDF ${file.basename}]`;
|
||||
}
|
||||
|
||||
// If not in cache, read the file and call the API
|
||||
const binaryContent = await vault.readBinary(file);
|
||||
logInfo("Calling pdf4llm API for:", file.path);
|
||||
|
|
|
|||
Loading…
Reference in a new issue