diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bad841..5f90c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,29 @@ and this project aims to adhere to [Semantic Versioning](https://semver.org/spec _(Future changes will go here)_ +## [0.7.12] - 2025-07-05 + +### Changed + +- **Migrated to genai-lite v0.1.3 Preset System**: Refactored model preset management to use genai-lite's configurable preset system with 'replace' mode. This eliminates code duplication while maintaining full control over Athanor's model configurations. ([275e792](https://github.com/lacerbi/athanor/commit/275e792)) + - Removed local `AthanorModelPreset` type and `athanorPresetService` + - Added IPC channel for fetching presets from the LLM service + - Updated UI components to use genai-lite's `ModelPreset` type + - Configuration now centralized through `src/config/athanorModelPresets.json` + +- **Migrated Template Rendering to genai-lite**: Refactored to use genai-lite's `renderTemplate` function for variable substitution in prompts. ([ac7d986](https://github.com/lacerbi/athanor/commit/ac7d986)) + - Removed local `substituteVariables` function from `promptTemplates.ts` + - Updated `buildPrompt.ts` to use `renderTemplate` from genai-lite + - Template rendering now provided by the shared library + +### Fixed + +- **Build Configuration**: Added JSON module resolution support to TypeScript and webpack configurations to properly handle preset configuration imports. + +### Dependencies + +- Updated `genai-lite` from v0.1.1 to v0.1.4 + ## [0.7.11] - 2025-07-04 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 596c2af..4ef771f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,6 +182,15 @@ Athanor is an Electron desktop application for AI-assisted development workflows - Conventional Commits for commit messages - **DCO Sign-off Required**: All commits must be signed with DCO. Always use `git commit -s` when committing. If git config is not set (check with `git config user.name` and `git config user.email`), inform the user that these need to be configured before committing. For fixing unsigned commits, use `git rebase --signoff` or `git commit --amend -s` +**Important Notes on External Package Imports:** + +When importing from `genai-lite` or similar external packages in the renderer process (`src/` directory): +- Use the package's exported paths as defined in their `package.json` exports field (e.g., `genai-lite/utils`) +- Add `// @ts-ignore - webpack module resolution issue` if TypeScript complains +- Do NOT use deep imports like `genai-lite/dist/utils/templateEngine` as webpack will reject these +- Check existing imports in the codebase for the correct pattern (e.g., see `RelevanceEngineService.ts`) +- Main process imports (`electron/` directory) typically work without issues + ## Key Dependencies - **Electron 33+** - Desktop app framework diff --git a/common/types/llm.ts b/common/types/llm.ts index 8f7028b..3d9d791 100644 --- a/common/types/llm.ts +++ b/common/types/llm.ts @@ -7,6 +7,7 @@ export const LLM_IPC_CHANNELS = { GET_MODELS: 'llm:get-models', SEND_MESSAGE: 'llm:send-message', IS_KEY_AVAILABLE: 'llm:is-key-available', + GET_PRESETS: 'llm:get-presets', } as const; /** diff --git a/electron/handlers/llmIpc.ts b/electron/handlers/llmIpc.ts index f477d2b..6f41325 100644 --- a/electron/handlers/llmIpc.ts +++ b/electron/handlers/llmIpc.ts @@ -1,5 +1,5 @@ // AI Summary: IPC handlers for LLM operations, routing requests between renderer and main process. -// Registers handlers for getting providers/models and sending messages to LLMs. +// Registers handlers for getting providers/models, sending messages to LLMs, and fetching presets. import { ipcMain } from 'electron'; import type { LLMService, LLMChatRequest, ApiProviderId } from 'genai-lite'; @@ -73,5 +73,15 @@ export function registerLlmIpc(llmService: LLMService, apiKeyService: ApiKeyServ } ); + // Handler for getting configured presets + ipcMain.handle(LLM_IPC_CHANNELS.GET_PRESETS, async () => { + try { + return llmService.getPresets(); + } catch (error) { + console.error('Error in GET_PRESETS handler:', error); + throw error; + } + }); + console.log('LLM IPC handlers registered successfully'); } diff --git a/electron/main.ts b/electron/main.ts index 2a4cf10..1421ca1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,6 +1,7 @@ // AI Summary: Main electron process that coordinates window management, file system operations, // and IPC communication between processes. Handles application lifecycle events, path resolution, // and uncaught exception handling with proper cleanup of file watchers. +// Configures genai-lite LLM service with Athanor's model presets using replace mode. import { app, BrowserWindow, Menu, nativeTheme, ipcMain } from 'electron'; import fixPath from 'fix-path'; import { Worker } from 'worker_threads'; @@ -14,7 +15,7 @@ import { ApiKeyServiceMain, registerSecureApiKeyIpc, } from 'genai-key-storage-lite'; -import { LLMService, type ApiKeyProvider } from 'genai-lite'; +import { LLMService, type ApiKeyProvider, type ModelPreset } from 'genai-lite'; import { RelevanceEngineService } from './services/RelevanceEngineService'; import { GitService } from './services/GitService'; import { UserActivityService } from './services/UserActivityService'; @@ -25,6 +26,7 @@ import { } from './services/ProjectGraphService'; import { PROJECT_ANALYSIS } from '../src/utils/constants'; import type { ApplicationSettings } from '../src/types/global'; +import athanorPresets from '../src/config/athanorModelPresets.json'; // --- WSL Graphics Fix Start --- // This addresses a specific rendering issue on WSL where Electron may default @@ -332,7 +334,10 @@ app.whenReady().then(async () => { return envKey || null; } }; - llmService = new LLMService(electronKeyProvider); + llmService = new LLMService(electronKeyProvider, { + presets: athanorPresets as ModelPreset[], + presetMode: 'replace' + }); // Register IPC handlers from the external package registerSecureApiKeyIpc(apiKeyService); diff --git a/electron/preload.ts b/electron/preload.ts index 131c4de..95ffebe 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -86,6 +86,7 @@ contextBridge.exposeInMainWorld('electronBridge', { getModels: (providerId: string) => ipcRenderer.invoke(LLM_IPC_CHANNELS.GET_MODELS, providerId), sendMessage: (request: any) => ipcRenderer.invoke(LLM_IPC_CHANNELS.SEND_MESSAGE, request), isKeyAvailable: (providerId: string) => ipcRenderer.invoke(LLM_IPC_CHANNELS.IS_KEY_AVAILABLE, providerId), + getPresets: () => ipcRenderer.invoke(LLM_IPC_CHANNELS.GET_PRESETS), }, userActivity: () => ipcRenderer.send('user-activity'), context: { diff --git a/electron/services/.summary_long.md b/electron/services/.summary_long.md index 58b463b..180b65d 100644 --- a/electron/services/.summary_long.md +++ b/electron/services/.summary_long.md @@ -131,6 +131,7 @@ Sophisticated context selection engine: - Incorporates UserActivityService signals - Respects GitService history - Applies TaskAnalysisUtils keywords +- Uses genai-lite/utils for token counting and smart preview ### TaskAnalysisUtils.ts @@ -230,19 +231,6 @@ Import path resolution: ## Utility Services -### PromptUtils.ts - -Token management utilities: - -**Core Functions:** -- `countTokens()`: GPT-4 tokenizer usage -- `getSmartPreview()`: Intelligent truncation - -**Smart Preview Algorithm:** -- Respects code boundaries -- Looks for natural breaks -- Configurable line limits - ### wordFilters.ts Keyword filtering data: diff --git a/electron/services/.summary_short.md b/electron/services/.summary_short.md index 056088a..e69c3d3 100644 --- a/electron/services/.summary_short.md +++ b/electron/services/.summary_short.md @@ -15,8 +15,7 @@ This directory contains core services that power Athanor's file system operation - **PathUtils.test.ts** - Unit tests for PathUtils validating cross-platform path manipulation functions. - **PathUtils.ts** - Stateless utilities for consistent cross-platform path manipulation with Unix-style normalization. - **ProjectGraphService.ts** - Analyzes project structure to build dependency graphs, file mentions, and commit relationships for relevance scoring. -- **PromptUtils.ts** - Utilities for prompt construction including token counting and smart content preview generation. -- **RelevanceEngineService.ts** - Two-phase scoring engine that identifies relevant context files using multiple heuristics and token budgeting. +- **RelevanceEngineService.ts** - Two-phase scoring engine that identifies relevant context files using multiple heuristics and token budgeting (uses genai-lite/utils for prompt utilities). - **SettingsService.ts** - Manages reading/writing of project and application settings JSON files with proper error handling. - **ShellService.ts** - Manages persistent pseudo-terminal sessions for integrated CLI functionality with buffered output. - **TaskAnalysisUtils.test.ts** - Unit tests for task analysis utilities validating keyword extraction and path mention detection. diff --git a/electron/services/PromptUtils.ts b/electron/services/PromptUtils.ts deleted file mode 100644 index e90b2fe..0000000 --- a/electron/services/PromptUtils.ts +++ /dev/null @@ -1,71 +0,0 @@ -// AI Summary: Provides utility functions for prompt construction, specifically token counting -// and smart content previewing, for use within the main process. - -import { Tiktoken, encodingForModel } from 'js-tiktoken'; - -let tokenizer: Tiktoken | null = null; - -// Initialize tokenizer with GPT-4 encoding (cl100k_base) -function getTokenizer(): Tiktoken { - if (!tokenizer) { - try { - tokenizer = encodingForModel('gpt-4'); - } catch (error) { - console.error('Failed to initialize tokenizer:', error); - throw error; // Re-throw to be handled by the calling function - } - } - return tokenizer; -} - -export function countTokens(text: string): number { - if (!text) { - return 0; - } - - try { - const enc = getTokenizer(); - if (!enc) { - console.error('Failed to get tokenizer'); - return 0; - } - const tokens = enc.encode(text); - return tokens.length; - } catch (error) { - console.error('Error counting tokens:', error); - return 0; - } -} - -// Smart content preview for non-selected files -export function getSmartPreview(content: string, config: { minLines: number; maxLines: number }): string { - const lines = content.split('\n'); - - // If the file is not longer than maxLines, return it in full - if (lines.length <= config.maxLines) { - return content; - } - - // Always show at least minLines - let endLine = config.minLines; - let emptyLinesCount = lines - .slice(0, config.minLines) - .filter((line) => line.trim() === '').length; - - // If we haven't found at least two empty lines, keep looking up to maxLines - if (emptyLinesCount < 2 && lines.length > config.minLines) { - for ( - let i = config.minLines; - i < Math.min(lines.length, config.maxLines); - i++ - ) { - if (lines[i].trim() === '') { - endLine = i + 1; // Include the empty line - break; - } - endLine = i + 1; - } - } - - return lines.slice(0, endLine).join('\n') + '\n... (content truncated)'; -} diff --git a/electron/services/RelevanceEngineService.ts b/electron/services/RelevanceEngineService.ts index d23ad75..e797670 100644 --- a/electron/services/RelevanceEngineService.ts +++ b/electron/services/RelevanceEngineService.ts @@ -6,7 +6,8 @@ import type { IGitService } from '../../common/types/git-service'; import { DependencyScanner } from './DependencyScanner'; import { PathUtils } from './PathUtils'; import { CONTEXT_BUILDER, SETTINGS } from '../../src/utils/constants'; -import * as PromptUtils from './PromptUtils'; +// @ts-ignore - webpack module resolution issue +import { countTokens, getSmartPreview } from 'genai-lite/utils'; import { ProjectGraphService } from './ProjectGraphService'; import { analyzeTaskDescription } from './TaskAnalysisUtils'; import { UserActivityService } from './UserActivityService'; @@ -354,11 +355,11 @@ export class RelevanceEngineService { const content = (await this.fileService.read(filePath, { encoding: 'utf-8', })) as string; - const preview = PromptUtils.getSmartPreview( + const preview = getSmartPreview( content, smartPreviewConfig ); - const tokenCount = PromptUtils.countTokens(preview); + const tokenCount = countTokens(preview); if (currentTokens + tokenCount <= options.maxNeighborTokens) { promptNeighbors.push(filePath); diff --git a/package-lock.json b/package-lock.json index 01482cf..e362462 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "athanor", - "version": "0.7.11", + "version": "0.7.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "athanor", - "version": "0.7.11", + "version": "0.7.12", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/sdk": "^0.52.0", @@ -20,7 +20,7 @@ "diff-match-patch": "^1.0.5", "fix-path": "^4.0.0", "genai-key-storage-lite": "^0.1.4", - "genai-lite": "^0.1.0", + "genai-lite": "^0.1.4", "ignore": "^7.0.0", "js-tiktoken": "^1.0.16", "lucide-react": "^0.469.0", @@ -9450,20 +9450,51 @@ } }, "node_modules/genai-lite": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/genai-lite/-/genai-lite-0.1.0.tgz", - "integrity": "sha512-o8axryP9nIA+UIJYT7QD4wW1sFEAudFpTjQIKc+xpH63CRA4A7sc8Yuivy/vDkYbpZa4f7D25zIvZCd6VrqWRw==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/genai-lite/-/genai-lite-0.1.4.tgz", + "integrity": "sha512-gsgwBxhzsmgxOFa/nGmBBwZMV70THC//fvj4OEKaeUDHhf5kxkIdl73hA8jUqjg8LqRZAhvZnixNqT6m4Eo3gw==", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.52.0", + "@anthropic-ai/sdk": "^0.56.0", "@google/genai": "^1.0.1", - "openai": "^4.103.0" + "js-tiktoken": "^1.0.20", + "openai": "^5.8.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/lacerbi" } }, + "node_modules/genai-lite/node_modules/@anthropic-ai/sdk": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.56.0.tgz", + "integrity": "sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==", + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, + "node_modules/genai-lite/node_modules/openai": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.8.2.tgz", + "integrity": "sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/generate-function": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", diff --git a/package.json b/package.json index 26b8aef..bdd15f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "athanor", - "version": "0.7.11", + "version": "0.7.12", "bugs": { "url": "https://github.com/lacerbi/athanor/issues" }, @@ -107,7 +107,7 @@ "diff-match-patch": "^1.0.5", "fix-path": "^4.0.0", "genai-key-storage-lite": "^0.1.4", - "genai-lite": "^0.1.0", + "genai-lite": "^0.1.4", "ignore": "^7.0.0", "js-tiktoken": "^1.0.16", "lucide-react": "^0.469.0", diff --git a/src/components/ApiKeyManagementPane.tsx b/src/components/ApiKeyManagementPane.tsx index ac00eee..a236671 100644 --- a/src/components/ApiKeyManagementPane.tsx +++ b/src/components/ApiKeyManagementPane.tsx @@ -1,6 +1,7 @@ // AI Summary: Dedicated component for secure API key management with provider selection, // key storage/deletion, validation, and display functionality. Uses secure storage via IPC bridge. // Updated for enhanced security - no longer has access to plaintext keys in renderer process. +// Fetches model presets from genai-lite LLM service to determine available providers. import React, { useEffect, useState, useCallback } from 'react'; import { HelpCircle, Eye, EyeOff, Save, Trash2, Check } from 'lucide-react'; @@ -9,7 +10,6 @@ import { ApiKeyServiceRenderer } from 'genai-key-storage-lite/renderer'; // Once common export is added to the package, use these imports: import type { ApiProvider } from 'genai-key-storage-lite/common'; import { ApiKeyStorageError } from 'genai-key-storage-lite/common'; -import { getAllAthanorPresets } from '../services/athanorPresetService'; const ApiKeyManagementPane: React.FC = () => { // API Key Management state @@ -96,7 +96,7 @@ const ApiKeyManagementPane: React.FC = () => { setKeyOpError(null); // Fetch Athanor presets and filter providers - getAllAthanorPresets() + window.electronBridge.llmService.getPresets() .then((presets) => { // Extract unique provider IDs from presets const presetProviderIdSet = new Set( diff --git a/src/components/action-panel/SendViaApiControls.tsx b/src/components/action-panel/SendViaApiControls.tsx index b46c79b..07e92c1 100644 --- a/src/components/action-panel/SendViaApiControls.tsx +++ b/src/components/action-panel/SendViaApiControls.tsx @@ -1,10 +1,10 @@ // AI Summary: Component for managing "Send via API" functionality including LLM preset selection, // API key validation, and sending prompts to LLM services with response processing. +// Uses genai-lite for model presets via the IPC bridge. import React, { useState, useEffect, useRef } from 'react'; import { Info } from 'lucide-react'; -import type { AthanorModelPreset } from '../../types/athanorPresets'; +import type { ModelPreset } from 'genai-lite'; import type { ApplicationSettings, LogEntry } from '../../types/global'; -import { getAllAthanorPresets } from '../../services/athanorPresetService'; import { processAiResponseContent } from '../../actions/ApplyAiOutputAction'; import { useApplyChangesStore } from '../../stores/applyChangesStore'; @@ -33,7 +33,7 @@ const SendViaApiControls: React.FC = ({ }) => { // State for LLM preset selection const [availablePresets, setAvailablePresets] = useState< - AthanorModelPreset[] + ModelPreset[] >([]); const [isLoadingPresets, setIsLoadingPresets] = useState(false); @@ -51,8 +51,8 @@ const SendViaApiControls: React.FC = ({ setIsLoadingPresets(true); try { - const allPresets = await getAllAthanorPresets(); - const filtered: AthanorModelPreset[] = []; + const allPresets = await window.electronBridge.llmService.getPresets(); + const filtered: ModelPreset[] = []; for (const preset of allPresets) { // Check if API key is available from any source for the provider diff --git a/src/services/athanorPresetService.ts b/src/services/athanorPresetService.ts deleted file mode 100644 index 9031563..0000000 --- a/src/services/athanorPresetService.ts +++ /dev/null @@ -1,239 +0,0 @@ -// AI Summary: Service for loading and managing Athanor model presets, filtering against available providers/models, -// with caching and validation. Integrates with LLM service via Electron bridge and provides logging. -import type { AthanorModelPreset } from '../types/athanorPresets'; -import { useLogStore } from '../stores/logStore'; - -// Import JSON data using require to avoid TypeScript module resolution issues -const athanorModelPresetsData = require('../config/athanorModelPresets.json'); -const rawPresets: AthanorModelPreset[] = athanorModelPresetsData as AthanorModelPreset[]; - -// Module-level cache and initialization tracking -let filteredPresetsCache: AthanorModelPreset[] | null = null; -let initializationPromise: Promise | null = null; - -/** - * Performs the actual initialization of presets by validating against available providers and models - */ -async function performInitialization(): Promise { - const { addLog } = useLogStore.getState(); - - // Check if the LLM service bridge is available - if (!window.electronBridge || !window.electronBridge.llmService) { - addLog("Error: LLM Service bridge not available. Cannot initialize Athanor model presets."); - filteredPresetsCache = []; - return; - } - - try { - addLog("Initializing Athanor model presets..."); - - // Fetch available providers and their models - const availableProviders = await window.electronBridge.llmService.getProviders(); - const validProviderIds = new Set(availableProviders.map(p => p.id)); - const validModelMap = new Map>(); - - // Build a map of provider -> valid models - for (const provider of availableProviders) { - try { - const models = await window.electronBridge.llmService.getModels(provider.id); - validModelMap.set(provider.id, new Set(models.map(m => m.id))); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - addLog(`Warning: Failed to fetch models for provider "${provider.id}": ${message}`); - validModelMap.set(provider.id, new Set()); // Set empty set for this provider - } - } - - // Filter presets against available providers and models - filteredPresetsCache = rawPresets.filter(preset => { - // Validate basic structure - if (!preset.id || !preset.displayName || !preset.providerId || !preset.modelId) { - addLog(`Warning: Athanor preset with incomplete data structure will be excluded. ID: ${preset.id || 'unknown'}`); - return false; - } - - // Check if provider is available - if (!validProviderIds.has(preset.providerId)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) references an unavailable provider "${preset.providerId}". It will be excluded.`); - return false; - } - - // Check if model is available for this provider - const providerModels = validModelMap.get(preset.providerId); - if (!providerModels || !providerModels.has(preset.modelId)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) references an unavailable model "${preset.modelId}" for provider "${preset.providerId}". It will be excluded.`); - return false; - } - - // Validate settings structure - if (preset.settings && typeof preset.settings !== 'object') { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) has invalid 'settings' format. It will be excluded.`); - return false; - } - - // Additional validation for specific settings - if (preset.settings) { - // Validate temperature - if (preset.settings.temperature !== undefined && - (typeof preset.settings.temperature !== 'number' || - preset.settings.temperature < 0 || - preset.settings.temperature > 2)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) has invalid temperature value. It will be excluded.`); - return false; - } - - // Validate maxTokens - if (preset.settings.maxTokens !== undefined && - (!Number.isInteger(preset.settings.maxTokens) || - preset.settings.maxTokens < 1 || - preset.settings.maxTokens > 100000)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) has invalid maxTokens value. It will be excluded.`); - return false; - } - - // Validate topP - if (preset.settings.topP !== undefined && - (typeof preset.settings.topP !== 'number' || - preset.settings.topP < 0 || - preset.settings.topP > 1)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) has invalid topP value. It will be excluded.`); - return false; - } - - // Validate Gemini safety settings if present - if (preset.settings.geminiSafetySettings && - !Array.isArray(preset.settings.geminiSafetySettings)) { - addLog(`Warning: Athanor preset "${preset.displayName}" (ID: ${preset.id}) has invalid geminiSafetySettings format. It will be excluded.`); - return false; - } - } - - return true; - }); - - const totalCount = rawPresets.length; - const validCount = filteredPresetsCache.length; - const excludedCount = totalCount - validCount; - - addLog(`Athanor model presets initialized. ${validCount} presets available${excludedCount > 0 ? ` (${excludedCount} excluded due to validation issues)` : ''}.`); - - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - addLog(`Error initializing Athanor model presets: ${message}`); - filteredPresetsCache = []; // Set to empty on error - } -} - -/** - * Ensures that the preset service is initialized before use - */ -function ensureInitialized(): Promise { - if (!initializationPromise) { - initializationPromise = performInitialization(); - } - return initializationPromise; -} - -/** - * Gets all available Athanor model presets - * @returns Promise resolving to array of valid presets - */ -export async function getAllAthanorPresets(): Promise { - await ensureInitialized(); - return [...(filteredPresetsCache || [])]; // Return a copy to prevent external modification -} - -/** - * Gets a specific Athanor model preset by ID - * @param id - The preset ID to look up - * @returns Promise resolving to the preset or undefined if not found - */ -export async function getAthanorPresetById(id: string): Promise { - await ensureInitialized(); - return filteredPresetsCache?.find(p => p.id === id); -} - -/** - * Gets all presets for a specific provider - * @param providerId - The provider ID to filter by - * @returns Promise resolving to array of presets for the provider - */ -export async function getAthanorPresetsByProvider(providerId: string): Promise { - await ensureInitialized(); - return (filteredPresetsCache || []).filter(p => p.providerId === providerId); -} - -/** - * Gets all presets for a specific model - * @param providerId - The provider ID - * @param modelId - The model ID to filter by - * @returns Promise resolving to array of presets for the model - */ -export async function getAthanorPresetsByModel(providerId: string, modelId: string): Promise { - await ensureInitialized(); - return (filteredPresetsCache || []).filter(p => p.providerId === providerId && p.modelId === modelId); -} - -/** - * Checks if presets are currently being initialized - * @returns True if initialization is in progress - */ -export function isInitializing(): boolean { - return initializationPromise !== null && filteredPresetsCache === null; -} - -/** - * Checks if presets have been initialized - * @returns True if initialization has completed (successfully or with errors) - */ -export function isInitialized(): boolean { - return filteredPresetsCache !== null; -} - -/** - * Forces re-initialization of the preset service - * This is useful when the available providers/models might have changed - * @returns Promise that resolves when re-initialization is complete - */ -export async function reinitializeAthanorModelPresetService(): Promise { - // Reset the cache and initialization state - filteredPresetsCache = null; - initializationPromise = null; - - // Perform fresh initialization - await ensureInitialized(); -} - -/** - * Explicitly initializes the Athanor model preset service - * This can be called during application startup to trigger loading - * @returns Promise that resolves when initialization is complete - */ -export async function initializeAthanorModelPresetService(): Promise { - await ensureInitialized(); -} - -/** - * Gets statistics about the preset service - * @returns Object containing initialization status and preset counts - */ -export async function getAthanorPresetServiceStats(): Promise<{ - isInitialized: boolean; - isInitializing: boolean; - totalRawPresets: number; - validPresets: number; - excludedPresets: number; -}> { - await ensureInitialized(); - - const validCount = filteredPresetsCache?.length || 0; - const totalCount = rawPresets.length; - - return { - isInitialized: isInitialized(), - isInitializing: isInitializing(), - totalRawPresets: totalCount, - validPresets: validCount, - excludedPresets: totalCount - validCount - }; -} diff --git a/src/types/athanorPresets.ts b/src/types/athanorPresets.ts deleted file mode 100644 index 8b6ba0e..0000000 --- a/src/types/athanorPresets.ts +++ /dev/null @@ -1,26 +0,0 @@ -// AI Summary: Defines TypeScript interface for Athanor model presets, combining provider/model references with custom LLM settings. -import type { ApiProviderId, LLMSettings } from 'genai-lite'; - -/** - * Represents an Athanor-specific model preset with pre-configured LLM settings - * optimized for common use cases within the application. - */ -export interface AthanorModelPreset { - /** Unique preset identifier, e.g., "gemini-pro-creative-writing" */ - id: string; - - /** User-friendly display name, e.g., "Google Gemini - gemini-1.5-pro-002 (Creative)" */ - displayName: string; - - /** Optional description of the preset's intended use case */ - description?: string; - - /** Provider ID that matches an entry in SUPPORTED_PROVIDERS from llm/main/config.ts */ - providerId: ApiProviderId; - - /** Model ID that matches a supported model for the given providerId */ - modelId: string; - - /** Preset-specific LLM settings, can include provider-specific configurations */ - settings: Partial; -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 4455966..ba92b03 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -258,6 +258,11 @@ declare global { * Checks if an API key is available from any source (secure storage or ENV). */ isKeyAvailable: (providerId: string) => Promise; + + /** + * Gets the configured model presets + */ + getPresets: () => Promise; }; userActivity: () => void; context: { diff --git a/src/utils/.summary_long.md b/src/utils/.summary_long.md index ecc69e1..45a6fd5 100644 --- a/src/utils/.summary_long.md +++ b/src/utils/.summary_long.md @@ -12,19 +12,18 @@ The utils directory contains the foundational utility functions that power Athan The main orchestrator for AI prompt generation: - **Dynamic Prompt Building**: Core function `buildDynamicPrompt` assembles complete prompts from templates - **Variable Management**: Handles 20+ template variables including project info, file contents, and task descriptions +- **Template Rendering**: Uses `renderTemplate` from genai-lite for variable substitution with conditional logic - **Configuration Integration**: Respects user settings for file tree inclusion, format type, and smart previews - **Supplementary Materials**: Separates regular files from materials using "materials:" prefix - **Format Flexibility**: Supports both XML and Markdown documentation formats - **Smart Preview Config**: Configurable preview lines (min/max) with fallback to constants #### promptTemplates.ts -Template engine with advanced variable substitution: +Template loading and task extraction utilities: - **Template Loading**: Async loading from resources directory via Electron IPC -- **Variable Substitution**: `{{variable}}` syntax with conditional ternary support -- **Conditional Logic**: `{{condition ? `true_value` : `false_value`}}` with backtick delimiters -- **Backward Compatibility**: Maintains support for legacy quote-based syntax - **Task Extraction**: Extracts task descriptions from XML tags in templates -- **Empty Value Handling**: Special handling for task_context to avoid empty sections +- **Error Handling**: Proper error propagation with descriptive messages +- **Path Resolution**: Uses main process for secure template path resolution #### codebaseDocumentation.ts Generates structured documentation for AI consumption: diff --git a/src/utils/.summary_short.md b/src/utils/.summary_short.md index 8aea1f8..2122785 100644 --- a/src/utils/.summary_short.md +++ b/src/utils/.summary_short.md @@ -4,7 +4,7 @@ Core utility functions for prompt building, file operations, and UI configuratio ## Files -- **buildPrompt.ts** - Builds AI prompts dynamically by loading templates, substituting variables, and applying user settings +- **buildPrompt.ts** - Builds AI prompts dynamically by loading templates, substituting variables via genai-lite, and applying user settings - **codebaseDocumentation.ts** - Generates formatted codebase documentation including file trees and code content with smart previews - **configUtils.ts** - Handles reading and resolving Athanor configuration with proper precedence for project settings - **constants.ts** - Stores configuration constants for file handling, UI animations, and application settings @@ -17,5 +17,5 @@ Core utility functions for prompt building, file operations, and UI configuratio - **fileTree.ts** - Core data structures and traversal utilities for the hierarchical file tree UI - **languageMapping.ts** - Maps file extensions to programming languages for syntax highlighting - **projectInfoUtils.ts** - Finds and reads project information files with case-insensitive search and content normalization -- **promptTemplates.ts** - Loads prompt templates and performs variable substitution with conditional logic support +- **promptTemplates.ts** - Loads prompt templates and extracts task descriptions from XML tags - **tokenCount.ts** - Counts tokens using js-tiktoken with efficient encoder caching and formatting utilities \ No newline at end of file diff --git a/src/utils/buildPrompt.ts b/src/utils/buildPrompt.ts index 48a36da..1887c46 100644 --- a/src/utils/buildPrompt.ts +++ b/src/utils/buildPrompt.ts @@ -6,9 +6,10 @@ import { generateCodebaseDocumentation } from './codebaseDocumentation'; import { DOC_FORMAT, FILE_SYSTEM, SETTINGS } from './constants'; import { loadTemplateContent, - substituteVariables, extractTaskDescription, } from './promptTemplates'; +// @ts-ignore - webpack module resolution issue +import { renderTemplate } from 'genai-lite/utils'; import { PromptData, PromptVariant } from '../types/promptTypes'; import { useFileSystemStore } from '../stores/fileSystemStore'; import { AthanorConfig } from '../types/global'; @@ -230,5 +231,5 @@ export async function buildDynamicPrompt( }; // Use the variant content directly - return substituteVariables(variant.content, variables); + return renderTemplate(variant.content, variables); } diff --git a/src/utils/promptTemplates.ts b/src/utils/promptTemplates.ts index b67b558..a1f1127 100644 --- a/src/utils/promptTemplates.ts +++ b/src/utils/promptTemplates.ts @@ -1,7 +1,6 @@ -// AI Summary: Handles loading prompt templates and substituting variables. +// AI Summary: Handles loading prompt templates. // Uses template paths resolved by the main process and properly handles empty values. -// Provides functions for template loading, variable substitution, and task description extraction. -import { PromptVariables } from './buildPrompt'; +// Provides functions for template loading and task description extraction. import { extractTagContent } from './extractTagContent'; // Load a template from the prompts folder export async function loadTemplateContent( @@ -23,78 +22,3 @@ export async function loadTemplateContent( export function extractTaskDescription(templateContent: string): string { return extractTagContent(templateContent, 'task_description'); } -// Substitute variables in template content -export function substituteVariables( - template: string, - variables: PromptVariables -): string { - return template.replace(/(\n?)\{\{([\s\S]+?)\}\}(\n?)/g, (match, leadingNewline: string, expression: string, trailingNewline: string) => { - const trimmedExpression = expression.trim(); - const conditionalMarkerIndex = trimmedExpression.indexOf('?'); - - let result: string; - - if (conditionalMarkerIndex === -1) { - // --- Simple variable substitution (backward compatible) --- - const key = trimmedExpression as keyof PromptVariables; - const value = variables[key]; - // Handle task context specially - only include if it exists - if (key === 'task_context' && (!value || (typeof value === 'string' && value.trim() === ''))) { - result = ''; - } else { - result = value !== undefined ? String(value) : ''; - } - } else { - // --- Conditional 'ternary' substitution --- - const conditionKey = trimmedExpression.substring(0, conditionalMarkerIndex).trim() as keyof PromptVariables; - const rest = trimmedExpression.substring(conditionalMarkerIndex + 1).trim(); - - // Parse ternary expression with backtick-delimited strings - // Expected format: condition ? `true_value` : `false_value` or condition ? `true_value` - const ternaryMatch = rest.match(/^\s*`([\s\S]*?)`(?:\s*:\s*`([\s\S]*?)`)?\s*$/); - - let trueText: string; - let falseText: string = ''; // Default to empty string if no 'else' part - - if (ternaryMatch) { - // Backtick format matched - unescape any escaped backticks - trueText = ternaryMatch[1].replace(/\\`/g, '`'); - if (ternaryMatch[2] !== undefined) { - falseText = ternaryMatch[2].replace(/\\`/g, '`'); - } - } else { - // Fallback to old quote-based parsing for backward compatibility - const elseMarkerIndex = rest.indexOf(':'); - - if (elseMarkerIndex === -1) { - trueText = rest; - } else { - trueText = rest.substring(0, elseMarkerIndex).trim(); - falseText = rest.substring(elseMarkerIndex + 1).trim(); - } - - // Remove quotes from the start and end of the text parts - const unquote = (text: string) => { - if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) { - return text.slice(1, -1); - } - return text; - }; - - trueText = unquote(trueText); - falseText = unquote(falseText); - } - - const conditionValue = !!variables[conditionKey]; // Evaluate truthiness - result = conditionValue ? trueText : falseText; - } - - // If result is empty, don't include the captured newlines - if (result === '') { - return ''; - } - - // If result is not empty, preserve the captured newlines - return leadingNewline + result + trailingNewline; - }); -} diff --git a/tsconfig.json b/tsconfig.json index 7c0ac5a..5cf54b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "strict": true, "skipLibCheck": true, "jsx": "react", + "resolveJsonModule": true, "baseUrl": "./", "paths": { "@components/*": ["src/components/*"], diff --git a/webpack.main.config.js b/webpack.main.config.js index c83f16c..771ecb1 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -18,10 +18,20 @@ module.exports = { ], use: [{ loader: 'ts-loader' }], }, + { + test: /\.json$/, + include: [ + path.resolve(__dirname, 'src/config'), + ], + type: 'json', + }, ], }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js', '.json'], + alias: { + 'genai-lite/utils': path.resolve(__dirname, 'node_modules/genai-lite/dist/utils/index.js'), + }, }, output: { path: path.resolve(__dirname, '.webpack'),