mirror of
https://github.com/aldo-g/obsidian-llm-test.git
synced 2026-07-22 05:42:19 +00:00
questions get back but not displayed correctly
This commit is contained in:
parent
8ebee35c75
commit
8eea6be16e
10 changed files with 391 additions and 275 deletions
81
TestView.ts
81
TestView.ts
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
This file implements a custom sidebar view for the Obsidian RAG Test Plugin.
|
||||
It displays a header with a "Prepare Tests" button that, when clicked, will trigger the indexing of test notes and prepare them to be sent to an LLM for test generation.
|
||||
Below the header, a file tree of test notes with status icons and test result counts is rendered.
|
||||
*/
|
||||
|
||||
import { App, ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import type { IndexedNote } from './main';
|
||||
|
||||
export const VIEW_TYPE = "rag-test-view";
|
||||
|
||||
/**
|
||||
* Custom sidebar view for displaying test notes and preparing tests.
|
||||
*/
|
||||
export default class TestView extends ItemView {
|
||||
pluginData: IndexedNote[];
|
||||
|
||||
/**
|
||||
* Constructs the TestView.
|
||||
* @param leaf The workspace leaf to attach the view.
|
||||
* @param app The Obsidian application instance.
|
||||
* @param pluginData The indexed notes data.
|
||||
*/
|
||||
constructor(leaf: WorkspaceLeaf, app: App, pluginData: IndexedNote[]) {
|
||||
super(leaf);
|
||||
this.pluginData = pluginData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view type.
|
||||
* @returns The view type identifier.
|
||||
*/
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display text for the view.
|
||||
* @returns The display text.
|
||||
*/
|
||||
getDisplayText(): string {
|
||||
return "Test Dashboard";
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is opened; triggers rendering.
|
||||
*/
|
||||
async onOpen(): Promise<void> {
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is closed.
|
||||
* @returns A promise that resolves when the view is closed.
|
||||
*/
|
||||
async onClose(): Promise<void> {
|
||||
// No additional cleanup required.
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the sidebar with a header containing a "Prepare Tests" button,
|
||||
* followed by a file tree of test notes with status icons and test result counts.
|
||||
*/
|
||||
render(): void {
|
||||
const container = this.containerEl;
|
||||
container.empty();
|
||||
const headerEl = container.createEl('div', { cls: 'test-view-header' });
|
||||
const prepareButton = headerEl.createEl('button', { text: 'Prepare Tests' });
|
||||
prepareButton.addEventListener('click', () => {
|
||||
console.log('Prepare Tests clicked');
|
||||
// Here you would call a function to index files and prepare data for the LLM.
|
||||
});
|
||||
const listEl = container.createEl('ul');
|
||||
this.pluginData.forEach((note) => {
|
||||
const itemEl = listEl.createEl('li');
|
||||
const statusIcon = note.testStatus.testsReady ? '🟢' : '🟡';
|
||||
const resultText = note.testStatus.testsReady ? ` (${note.testStatus.passed}/${note.testStatus.total})` : '';
|
||||
itemEl.createEl('span', { text: `${statusIcon} ${note.filePath}${resultText}` });
|
||||
});
|
||||
}
|
||||
}
|
||||
106
main.ts
106
main.ts
|
|
@ -1,11 +1,14 @@
|
|||
/*
|
||||
This file is the main plugin file for the Obsidian RAG Test Plugin.
|
||||
It registers a ribbon icon that, when clicked, opens the custom sidebar view (Test Dashboard).
|
||||
It also handles indexing of test notes in the "Test" folder and persisting the index.
|
||||
It registers a ribbon icon that, when clicked, opens the dashboard view for selecting notes.
|
||||
It also registers the full-screen Question Document view and exposes a method to open that view.
|
||||
It handles indexing of notes, persisting settings, and provides a settings tab for configuration.
|
||||
*/
|
||||
|
||||
import { App, Notice, Plugin, PluginSettingTab, Setting, TFile, WorkspaceLeaf } from 'obsidian';
|
||||
import TestView, { VIEW_TYPE } from './TestView';
|
||||
import { App, Notice, Plugin, PluginSettingTab, Setting, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import TestDashboardView, { VIEW_TYPE as DASHBOARD_VIEW_TYPE } from "./src/components/TestDashboardView";
|
||||
import QuestionDocumentView, { QUESTION_VIEW_TYPE } from "./src/components/QuestionDocumentView";
|
||||
import SettingsTab from "./src/components/SettingsTab";
|
||||
|
||||
/**
|
||||
* Interface for test status.
|
||||
|
|
@ -27,10 +30,12 @@ export interface IndexedNote {
|
|||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
mySetting: "default",
|
||||
apiKey: ""
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -47,39 +52,42 @@ interface MyPluginData {
|
|||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
indexedNotes: IndexedNote[] = [];
|
||||
testViewLeaf: WorkspaceLeaf | null = null;
|
||||
testDashboardLeaf: WorkspaceLeaf | null = null;
|
||||
|
||||
/**
|
||||
* Called when the plugin is loaded.
|
||||
*/
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.registerView(VIEW_TYPE, (leaf: WorkspaceLeaf) => new TestView(leaf, this.app, this.indexedNotes));
|
||||
this.addRibbonIcon('dice', 'Test Dashboard', (evt: MouseEvent) => {
|
||||
// Register dashboard view for note selection.
|
||||
this.registerView(DASHBOARD_VIEW_TYPE, (leaf: WorkspaceLeaf) => new TestDashboardView(leaf, this.app, this.indexedNotes));
|
||||
// Register full-screen question document view.
|
||||
this.registerView(QUESTION_VIEW_TYPE, (leaf: WorkspaceLeaf) => new QuestionDocumentView(leaf, this.app, []));
|
||||
|
||||
this.addRibbonIcon("dice", "Test Dashboard", (evt: MouseEvent) => {
|
||||
this.openTestDashboard();
|
||||
});
|
||||
this.addStatusBarItem().setText('RAG Test Plugin Active');
|
||||
this.addStatusBarItem().setText("RAG Test Plugin Active");
|
||||
this.addCommand({
|
||||
id: 'open-test-dashboard',
|
||||
name: 'Open Test Dashboard',
|
||||
id: "open-test-dashboard",
|
||||
name: "Open Test Dashboard",
|
||||
callback: () => {
|
||||
this.openTestDashboard();
|
||||
}
|
||||
});
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('Document clicked', evt);
|
||||
this.addSettingTab(new SettingsTab(this.app, this));
|
||||
this.registerDomEvent(document, "click", (evt: MouseEvent) => {
|
||||
console.log("Document clicked", evt);
|
||||
});
|
||||
this.registerInterval(window.setInterval(() => console.log('Interval log'), 5 * 60 * 1000));
|
||||
this.registerInterval(window.setInterval(() => console.log("Interval log"), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is unloaded.
|
||||
*/
|
||||
onunload() {
|
||||
if (this.testViewLeaf) {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
|
||||
}
|
||||
this.app.workspace.detachLeavesOfType(DASHBOARD_VIEW_TYPE);
|
||||
this.app.workspace.detachLeavesOfType(QUESTION_VIEW_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,11 +116,12 @@ export default class MyPlugin extends Plugin {
|
|||
}
|
||||
|
||||
/**
|
||||
* Indexes Markdown files in the "Test" folder, extracts test readiness data, and persists the index.
|
||||
* Indexes all Markdown files in the vault, extracts test readiness data, and persists the index.
|
||||
*/
|
||||
async indexTestNotes() {
|
||||
this.indexedNotes = [];
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles().filter((file: TFile) => file.path.startsWith("Test/"));
|
||||
// Scan all Markdown files without filtering by folder.
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles();
|
||||
for (const file of markdownFiles) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const testsReady = content.includes("## Test");
|
||||
|
|
@ -131,59 +140,44 @@ export default class MyPlugin extends Plugin {
|
|||
console.log(`Indexed: ${file.path}`);
|
||||
}
|
||||
await this.saveSettings();
|
||||
new Notice(`Indexed ${this.indexedNotes.length} test notes`);
|
||||
new Notice(`Indexed ${this.indexedNotes.length} notes`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the test dashboard view in the left sidebar.
|
||||
*/
|
||||
openTestDashboard() {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
|
||||
this.app.workspace.detachLeavesOfType(DASHBOARD_VIEW_TYPE);
|
||||
const leaf = this.app.workspace.getLeftLeaf(false);
|
||||
if (!leaf) {
|
||||
new Notice('Could not obtain workspace leaf.');
|
||||
new Notice("Could not obtain workspace leaf.");
|
||||
return;
|
||||
}
|
||||
leaf.setViewState({
|
||||
type: VIEW_TYPE,
|
||||
type: DASHBOARD_VIEW_TYPE,
|
||||
active: true
|
||||
});
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
this.testViewLeaf = leaf;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings tab for the RAG Test Plugin.
|
||||
*/
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
/**
|
||||
* Constructs the settings tab.
|
||||
* @param app The Obsidian application instance.
|
||||
* @param plugin The plugin instance.
|
||||
*/
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.testDashboardLeaf = leaf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings tab.
|
||||
* Opens a full-screen question document view with the provided test questions.
|
||||
* @param questions - An array of test question strings.
|
||||
*/
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc("It's a secret")
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
public openQuestionDocument(questions: string[]): void {
|
||||
this.app.workspace.detachLeavesOfType(QUESTION_VIEW_TYPE);
|
||||
const leaf = this.app.workspace.getRightLeaf(false);
|
||||
if (!leaf) {
|
||||
new Notice("Could not obtain workspace leaf for question document.");
|
||||
return;
|
||||
}
|
||||
leaf.setViewState({
|
||||
type: QUESTION_VIEW_TYPE,
|
||||
active: true
|
||||
});
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
// Instantiate the full-screen view with the questions.
|
||||
new QuestionDocumentView(leaf, this.app, questions);
|
||||
}
|
||||
}
|
||||
98
src/components/QuestionDocumentView.ts
Normal file
98
src/components/QuestionDocumentView.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
This file implements a full-page view for displaying generated test questions.
|
||||
It renders an interactive document where each question is displayed with an input field for the user's answer,
|
||||
and includes a "Mark Test" button to submit the answers.
|
||||
*/
|
||||
|
||||
import { App, ItemView, Notice, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
export const QUESTION_VIEW_TYPE = "question-document-view";
|
||||
|
||||
/**
|
||||
* A full-page view for displaying test questions.
|
||||
*/
|
||||
export default class QuestionDocumentView extends ItemView {
|
||||
questions: string[];
|
||||
|
||||
/**
|
||||
* Constructs the QuestionDocumentView.
|
||||
* @param leaf - The workspace leaf to attach the view.
|
||||
* @param app - The Obsidian application instance.
|
||||
* @param questions - An array of test questions.
|
||||
*/
|
||||
constructor(leaf: WorkspaceLeaf, app: App, questions: string[]) {
|
||||
super(leaf);
|
||||
this.questions = questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view type identifier.
|
||||
* @returns The view type string.
|
||||
*/
|
||||
getViewType(): string {
|
||||
return QUESTION_VIEW_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display text for the view.
|
||||
* @returns The display text.
|
||||
*/
|
||||
getDisplayText(): string {
|
||||
return "Generated Test Questions";
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is opened; triggers rendering.
|
||||
*/
|
||||
async onOpen(): Promise<void> {
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is closed.
|
||||
* @returns A promise that resolves when the view is closed.
|
||||
*/
|
||||
async onClose(): Promise<void> {
|
||||
// No additional cleanup required.
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the interactive question document.
|
||||
*/
|
||||
render(): void {
|
||||
const container = this.containerEl;
|
||||
container.empty();
|
||||
|
||||
const titleEl = container.createEl("h1", { text: "Generated Test Questions" });
|
||||
const formEl = container.createEl("form");
|
||||
|
||||
this.questions.forEach((question, index) => {
|
||||
const questionDiv = formEl.createEl("div", { cls: "question-item" });
|
||||
const label = questionDiv.createEl("label", { text: `Q${index + 1}: ${question}` });
|
||||
label.style.display = "block";
|
||||
const input = questionDiv.createEl("input", { type: "text" }) as HTMLInputElement;
|
||||
input.placeholder = "Type your answer here";
|
||||
input.style.width = "100%";
|
||||
(input as HTMLElement).dataset.questionIndex = index.toString();
|
||||
});
|
||||
|
||||
const markButton = formEl.createEl("button", { text: "Mark Test" });
|
||||
markButton.type = "button";
|
||||
markButton.addEventListener("click", () => {
|
||||
const answers: { [key: number]: string } = {};
|
||||
const inputs = formEl.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
|
||||
inputs.forEach((input) => {
|
||||
const idxStr = input.dataset.questionIndex;
|
||||
if (idxStr !== undefined) {
|
||||
const idx = parseInt(idxStr, 10);
|
||||
answers[idx] = input.value;
|
||||
}
|
||||
});
|
||||
new Notice("Test answers submitted!");
|
||||
console.log("User Answers:", answers);
|
||||
});
|
||||
|
||||
formEl.appendChild(markButton);
|
||||
container.appendChild(formEl);
|
||||
}
|
||||
}
|
||||
57
src/components/SettingsTab.ts
Normal file
57
src/components/SettingsTab.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
This file implements the settings tab for the Obsidian RAG Test Plugin.
|
||||
It allows the user to configure plugin options, including setting the OpenAI API key.
|
||||
*/
|
||||
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type { IndexedNote } from "../models/types";
|
||||
import type MyPlugin from "../../main";
|
||||
|
||||
/**
|
||||
* Settings tab for the RAG Test Plugin.
|
||||
*/
|
||||
export default class SettingsTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
/**
|
||||
* Constructs the settings tab.
|
||||
* @param app - The Obsidian application instance.
|
||||
* @param plugin - The main plugin instance.
|
||||
*/
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings tab.
|
||||
*/
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
new Setting(containerEl)
|
||||
.setName("Setting #1")
|
||||
.setDesc("It's a secret")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Enter your secret")
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("OpenAI API Key")
|
||||
.setDesc("Enter your OpenAI API key for test generation.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("sk-...")
|
||||
.setValue(this.plugin.settings.apiKey)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.apiKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
147
src/components/TestDashboardView.ts
Normal file
147
src/components/TestDashboardView.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
This file implements a custom sidebar view (dashboard) for the Obsidian RAG Test Plugin.
|
||||
It displays a list of all indexed notes with checkboxes for selection and a "Create Tests" button.
|
||||
When "Create Tests" is clicked, an OpenAI API call is made for each selected note (using that note's full content)
|
||||
to generate test questions. All generated questions are then aggregated and displayed in a full-screen question document view.
|
||||
*/
|
||||
|
||||
import { App, ItemView, Notice, WorkspaceLeaf } from "obsidian";
|
||||
import type { IndexedNote } from "../models/types";
|
||||
import { generateTestQuestions } from "../services/llm";
|
||||
|
||||
export const VIEW_TYPE = "rag-test-view";
|
||||
|
||||
/**
|
||||
* Custom dashboard view for selecting notes and generating tests.
|
||||
*/
|
||||
export default class TestDashboardView extends ItemView {
|
||||
pluginData: IndexedNote[];
|
||||
|
||||
/**
|
||||
* Constructs the dashboard view.
|
||||
* @param leaf - The workspace leaf to attach the view.
|
||||
* @param app - The Obsidian application instance.
|
||||
* @param pluginData - The array of indexed notes.
|
||||
*/
|
||||
constructor(leaf: WorkspaceLeaf, app: App, pluginData: IndexedNote[]) {
|
||||
super(leaf);
|
||||
this.pluginData = pluginData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view type identifier.
|
||||
* @returns The view type string.
|
||||
*/
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display text for the view.
|
||||
* @returns The display text.
|
||||
*/
|
||||
getDisplayText(): string {
|
||||
return "Test Dashboard";
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is opened; triggers rendering.
|
||||
*/
|
||||
async onOpen(): Promise<void> {
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is closed.
|
||||
* @returns A promise that resolves when the view is closed.
|
||||
*/
|
||||
async onClose(): Promise<void> {
|
||||
// No cleanup needed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dashboard: a list of notes with selection checkboxes and a "Create Tests" button.
|
||||
*/
|
||||
async render(): Promise<void> {
|
||||
const container = this.containerEl;
|
||||
container.empty();
|
||||
|
||||
// Header with "Create Tests" button
|
||||
const headerEl = container.createEl("div", { cls: "test-view-header" });
|
||||
const createTestsButton = headerEl.createEl("button", { text: "Create Tests" });
|
||||
createTestsButton.disabled = true;
|
||||
createTestsButton.addEventListener("click", async () => {
|
||||
await this.createTests();
|
||||
});
|
||||
|
||||
// Container for the list of notes
|
||||
const listEl = container.createEl("ul");
|
||||
this.pluginData.forEach((note) => {
|
||||
const itemEl = listEl.createEl("li");
|
||||
const checkbox = itemEl.createEl("input", { type: "checkbox" });
|
||||
checkbox.dataset.filePath = note.filePath;
|
||||
itemEl.createEl("span", { text: ` ${note.filePath}` });
|
||||
checkbox.addEventListener("change", () => {
|
||||
this.updateCreateTestsButtonState(createTestsButton, listEl);
|
||||
});
|
||||
});
|
||||
|
||||
this.updateCreateTestsButtonState(createTestsButton, listEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the enabled state of the "Create Tests" button based on selection.
|
||||
* @param button - The button element.
|
||||
* @param listEl - The container element for the checkboxes.
|
||||
*/
|
||||
updateCreateTestsButtonState(button: HTMLButtonElement, listEl: HTMLElement): void {
|
||||
const checkboxes = listEl.querySelectorAll('input[type="checkbox"]');
|
||||
let anyChecked = false;
|
||||
checkboxes.forEach((cb) => {
|
||||
if ((cb as HTMLInputElement).checked) {
|
||||
anyChecked = true;
|
||||
}
|
||||
});
|
||||
button.disabled = !anyChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each selected note, calls the LLM API to generate test questions,
|
||||
* aggregates all questions, and then opens a full-screen view to display them.
|
||||
*/
|
||||
async createTests(): Promise<void> {
|
||||
const container = this.containerEl;
|
||||
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
// Retrieve API key from plugin settings stored in the main plugin instance.
|
||||
const plugin = (this.app as any).plugins.plugins["obsidian-rag-test-plugin"] as any;
|
||||
const apiKey = plugin?.settings?.apiKey;
|
||||
if (!apiKey) {
|
||||
new Notice("❌ OpenAI API Key is missing! Please set it in the plugin settings.");
|
||||
return;
|
||||
}
|
||||
let aggregatedQuestions: string[] = [];
|
||||
for (const checkbox of Array.from(checkboxes)) {
|
||||
const input = checkbox as HTMLInputElement;
|
||||
if (input.checked) {
|
||||
const filePath = input.dataset.filePath;
|
||||
const note = this.pluginData.find(n => n.filePath === filePath);
|
||||
if (!note) continue;
|
||||
console.log(`Generating tests for: ${filePath}`);
|
||||
try {
|
||||
const questions = await generateTestQuestions([note], apiKey);
|
||||
console.log(`Generated tests for ${filePath}:`, questions);
|
||||
aggregatedQuestions.push(`Questions for ${filePath}:`);
|
||||
aggregatedQuestions.push(...questions);
|
||||
} catch (error) {
|
||||
console.error(`Error generating tests for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregatedQuestions.length > 0) {
|
||||
// Open a full-screen view (new page) to display the aggregated test questions.
|
||||
(plugin as any).openQuestionDocument(aggregatedQuestions);
|
||||
} else {
|
||||
new Notice("No tests generated.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,28 @@
|
|||
/*
|
||||
This file contains shared type definitions for the Obsidian RAG Test Plugin,
|
||||
including types for LLM responses and generated tests.
|
||||
including types for LLM responses, generated tests, and indexed notes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the response structure from the LLM API.
|
||||
*/
|
||||
export interface LLMResponse {
|
||||
data: any;
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: {
|
||||
index: number;
|
||||
message: {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
}[];
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,26 +50,3 @@ export interface IndexedNote {
|
|||
content: string;
|
||||
testStatus: TestStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared type definitions for indexed notes and LLM responses.
|
||||
*/
|
||||
|
||||
export interface LLMResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: {
|
||||
index: number;
|
||||
message: {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
}[];
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ This file contains functions to format indexed notes into a prompt for an LLM.
|
|||
It exports a function formatNotesForLLM that takes an array of IndexedNote and returns a formatted prompt string.
|
||||
*/
|
||||
|
||||
import type { IndexedNote } from "./types";
|
||||
import type { IndexedNote } from "../models/types";
|
||||
|
||||
/**
|
||||
* Formats an array of IndexedNote objects into a structured prompt string for the LLM.
|
||||
|
|
@ -18,4 +18,4 @@ export function formatNotesForLLM(notes: IndexedNote[]): string {
|
|||
});
|
||||
prompt += "Ensure that the test questions are relevant to the content and test key concepts.";
|
||||
return prompt;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ extracts test-related markers (if any), and returns an array of IndexedNote obje
|
|||
*/
|
||||
|
||||
import { App } from "obsidian";
|
||||
import type { IndexedNote, TestStatus } from "./types";
|
||||
import type { IndexedNote, TestStatus } from "../models/types";
|
||||
|
||||
/**
|
||||
* Scans all Markdown files in the vault and extracts test data.
|
||||
|
|
@ -38,4 +38,4 @@ export async function indexTestNotes(app: App): Promise<IndexedNote[]> {
|
|||
});
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,39 @@
|
|||
/**
|
||||
* Handles communication with OpenAI to generate test questions.
|
||||
*/
|
||||
/*
|
||||
This file handles communication with OpenAI to generate test questions.
|
||||
It exports a function generateTestQuestions that sends a prompt (built using formatter.ts)
|
||||
to OpenAI using the provided API key and returns an array of test questions.
|
||||
*/
|
||||
|
||||
import { formatNotesForLLM } from "./formatter";
|
||||
import type { IndexedNote, LLMResponse } from "./types";
|
||||
import type { IndexedNote, LLMResponse } from "../models/types";
|
||||
|
||||
const API_URL = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
// Load API key from environment variables (DO NOT hardcode!)
|
||||
import { config } from "dotenv";
|
||||
config(); // Load .env variables
|
||||
|
||||
const API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!API_KEY) {
|
||||
throw new Error("Missing OpenAI API key! Add it to your .env file.");
|
||||
}
|
||||
|
||||
console.log("✅ OpenAI API Key Loaded Successfully");
|
||||
|
||||
/**
|
||||
* Sends formatted note data to OpenAI and retrieves test questions.
|
||||
* @param {IndexedNote[]} indexedNotes - The indexed notes to generate questions from.
|
||||
* @returns {Promise<string[]>} - The generated test questions.
|
||||
* @param indexedNotes - The indexed notes to generate questions from.
|
||||
* @param apiKey - The OpenAI API key.
|
||||
* @returns A promise that resolves to an array of generated test questions.
|
||||
*/
|
||||
export async function generateTestQuestions(indexedNotes: IndexedNote[]): Promise<string[]> {
|
||||
export async function generateTestQuestions(indexedNotes: IndexedNote[], apiKey: string): Promise<string[]> {
|
||||
if (!apiKey) {
|
||||
throw new Error("Missing OpenAI API key! Please set it in the plugin settings.");
|
||||
}
|
||||
const prompt = formatNotesForLLM(indexedNotes);
|
||||
|
||||
const requestBody = {
|
||||
model: "gpt-4-turbo", // You can change this model
|
||||
model: "gpt-3.5-turbo-0125",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful AI that generates test questions from study notes." },
|
||||
{ role: "system", content: "You are a helpful AI that generates test questions from study notes. Your questions should be answered with written answers." },
|
||||
{ role: "user", content: prompt }
|
||||
],
|
||||
temperature: 0.7, // Adjust for more or less creativity
|
||||
temperature: 0.7,
|
||||
max_tokens: 500
|
||||
};
|
||||
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${API_KEY}`,
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
|
|
@ -59,4 +52,4 @@ export async function generateTestQuestions(indexedNotes: IndexedNote[]): Promis
|
|||
|
||||
// Split response into separate questions (assuming they are newline-separated)
|
||||
return output.split("\n").filter(q => q.trim().length > 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/**
|
||||
* Test script to verify the indexing, formatting, and LLM response generation.
|
||||
* It mocks an Obsidian vault, runs the indexer and formatter, calls the LLM, and prints the output.
|
||||
*/
|
||||
|
||||
import { indexTestNotes } from "./src/indexer";
|
||||
import { formatNotesForLLM } from "./src/formatter";
|
||||
import { generateTestQuestions } from "./src/llm";
|
||||
import type { IndexedNote } from "./src/types";
|
||||
|
||||
// Mock Vault class to simulate Obsidian vault behavior
|
||||
class MockVault {
|
||||
files: { path: string; content: string }[];
|
||||
|
||||
constructor(files: { path: string; content: string }[]) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
// Simulates fetching Markdown files
|
||||
getMarkdownFiles() {
|
||||
return this.files.map(file => ({
|
||||
path: file.path,
|
||||
}));
|
||||
}
|
||||
|
||||
// Simulates reading a file's content
|
||||
async read(file: { path: string }) {
|
||||
const found = this.files.find(f => f.path === file.path);
|
||||
if (found) {
|
||||
return found.content;
|
||||
}
|
||||
throw new Error(`File not found: ${file.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mock App object
|
||||
class MockApp {
|
||||
vault: MockVault;
|
||||
|
||||
constructor(vault: MockVault) {
|
||||
this.vault = vault;
|
||||
}
|
||||
}
|
||||
|
||||
// Sample test data (simulating Obsidian Markdown files)
|
||||
const mockFiles = [
|
||||
{
|
||||
path: "Test/1.1 - The Art of Learning.md",
|
||||
content: "## Key Takeaways\n- Learning is an iterative process.\n- Experimentation is crucial.\n\n## Test\n- [ ] What is the key principle of learning?",
|
||||
},
|
||||
{
|
||||
path: "Test/1.2 - The Pyramid Principle.md",
|
||||
content: "## Summary\nThe Pyramid Principle helps structure arguments.\n\n## Test\n- [x] What does the Pyramid Principle help with?",
|
||||
},
|
||||
{
|
||||
path: "General Notes/1.3 - Random Thoughts.md",
|
||||
content: "## Notes\nSome random reflections on life.",
|
||||
},
|
||||
];
|
||||
|
||||
async function runTest() {
|
||||
// Initialize mock app and vault
|
||||
const mockApp = new MockApp(new MockVault(mockFiles));
|
||||
|
||||
// Step 1: Run indexer
|
||||
const indexedNotes: IndexedNote[] = await indexTestNotes(mockApp as any);
|
||||
console.log("\n📂 Indexed Notes:\n", indexedNotes);
|
||||
|
||||
// Step 2: Run formatter
|
||||
const formattedPrompt = formatNotesForLLM(indexedNotes);
|
||||
console.log("\n📝 Formatted Prompt for LLM:\n", formattedPrompt);
|
||||
|
||||
// Step 3: Call LLM to generate test questions
|
||||
try {
|
||||
console.log("\n🚀 Sending request to OpenAI...\n");
|
||||
const testQuestions = await generateTestQuestions(indexedNotes);
|
||||
console.log("\n✅ Generated Test Questions:\n", testQuestions);
|
||||
} catch (error) {
|
||||
console.error("\n❌ Error in LLM call:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the test
|
||||
runTest();
|
||||
Loading…
Reference in a new issue