Files
archon/archon-ui-main/src/features/knowledge/services/knowledgeService.ts
leex279 98b798173e feat: Implement comprehensive OpenAI error handling for Issue #362
Replace silent failures with clear, actionable error messages to eliminate
90-minute debugging sessions when OpenAI API quota is exhausted.

## Backend Enhancements
- Add error sanitization preventing sensitive data exposure (API keys, URLs, tokens)
- Add upfront API key validation before expensive operations (crawl, upload, refresh)
- Implement fail-fast pattern in RAG service (no more empty results for API failures)
- Add specific error handling for quota, rate limit, auth, and API errors
- Add EmbeddingAuthenticationError exception with masked key prefix support

## Frontend Enhancements
- Create enhanced error utilities with OpenAI-specific parsing
- Build TanStack Query compatible API wrapper preserving ETag caching
- Update knowledge service to use enhanced error handling
- Enhance TanStack Query hooks with user-friendly error messages

## Security Features
- Comprehensive regex sanitization (8 patterns) with ReDoS protection
- Input validation and circular reference detection
- Generic fallback messages for sensitive keywords
- Bounded quantifiers to prevent regex DoS attacks

## User Experience
- Clear error messages: "OpenAI API quota exhausted"
- Actionable guidance: "Check your OpenAI billing dashboard and add credits"
- Immediate error visibility (no more silent failures)
- Appropriate error severity styling

## Architecture Compatibility
- Full TanStack Query integration maintained
- ETag caching and optimistic updates preserved
- No performance regression (all existing tests pass)
- Compatible with existing knowledge base architecture

Resolves #362: Users no longer experience mysterious empty RAG results
that require extensive debugging to identify OpenAI quota issues.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 19:22:36 +02:00

224 lines
6.6 KiB
TypeScript

/**
* Knowledge Base Service
* Handles all knowledge-related API operations using TanStack Query patterns
*/
import { invalidateETagCache } from "../../projects/shared/apiWithEtag";
import { callKnowledgeAPI, uploadWithEnhancedErrors } from "./apiWithEnhancedErrors";
import type {
ChunksResponse,
CodeExamplesResponse,
CrawlRequest,
CrawlStartResponse,
KnowledgeItem,
KnowledgeItemsFilter,
KnowledgeItemsResponse,
KnowledgeSource,
RefreshResponse,
SearchOptions,
SearchResultsResponse,
UploadMetadata,
} from "../types";
export const knowledgeService = {
/**
* Get lightweight summaries of knowledge items
* Use this for card displays and frequent updates
*/
async getKnowledgeSummaries(filter?: KnowledgeItemsFilter): Promise<KnowledgeItemsResponse> {
const params = new URLSearchParams();
if (filter?.page) params.append("page", filter.page.toString());
if (filter?.per_page) params.append("per_page", filter.per_page.toString());
if (filter?.knowledge_type) params.append("knowledge_type", filter.knowledge_type);
if (filter?.search) params.append("search", filter.search);
if (filter?.tags?.length) {
for (const tag of filter.tags) {
params.append("tags", tag);
}
}
const queryString = params.toString();
const endpoint = `/api/knowledge-items/summary${queryString ? `?${queryString}` : ""}`;
return callKnowledgeAPI<KnowledgeItemsResponse>(endpoint);
},
/**
* Get a specific knowledge item
*/
async getKnowledgeItem(sourceId: string): Promise<KnowledgeItem> {
return callKnowledgeAPI<KnowledgeItem>(`/api/knowledge-items/${sourceId}`);
},
/**
* Delete a knowledge item
*/
async deleteKnowledgeItem(sourceId: string): Promise<{ success: boolean; message: string }> {
const response = await callKnowledgeAPI<{ success: boolean; message: string }>(`/api/knowledge-items/${sourceId}`, {
method: "DELETE",
});
// Invalidate cache after deletion
invalidateETagCache("/api/knowledge-items");
invalidateETagCache("/api/knowledge-items/summary");
invalidateETagCache(`/api/knowledge-items/${sourceId}`);
return response;
},
/**
* Update a knowledge item
*/
async updateKnowledgeItem(sourceId: string, updates: Partial<KnowledgeItem>): Promise<KnowledgeItem> {
const response = await callKnowledgeAPI<KnowledgeItem>(`/api/knowledge-items/${sourceId}`, {
method: "PUT",
body: JSON.stringify(updates),
});
// Invalidate both list and specific item cache
invalidateETagCache("/api/knowledge-items");
invalidateETagCache("/api/knowledge-items/summary");
invalidateETagCache(`/api/knowledge-items/${sourceId}`);
return response;
},
/**
* Start crawling a URL
*/
async crawlUrl(request: CrawlRequest): Promise<CrawlStartResponse> {
const response = await callKnowledgeAPI<CrawlStartResponse>("/api/knowledge-items/crawl", {
method: "POST",
body: JSON.stringify(request),
});
// Invalidate list cache as new item will be added
invalidateETagCache("/api/knowledge-items");
invalidateETagCache("/api/knowledge-items/summary");
return response;
},
/**
* Refresh an existing knowledge item
*/
async refreshKnowledgeItem(sourceId: string): Promise<RefreshResponse> {
const response = await callKnowledgeAPI<RefreshResponse>(`/api/knowledge-items/${sourceId}/refresh`, {
method: "POST",
});
// Invalidate caches
invalidateETagCache("/api/knowledge-items");
invalidateETagCache("/api/knowledge-items/summary");
invalidateETagCache(`/api/knowledge-items/${sourceId}`);
return response;
},
/**
* Upload a document
*/
async uploadDocument(
file: File,
metadata: UploadMetadata,
): Promise<{ success: boolean; progressId: string; message: string; filename: string }> {
const formData = new FormData();
formData.append("file", file);
if (metadata.knowledge_type) {
formData.append("knowledge_type", metadata.knowledge_type);
}
if (metadata.tags?.length) {
formData.append("tags", JSON.stringify(metadata.tags));
}
// Use enhanced upload wrapper with OpenAI error handling
const result = await uploadWithEnhancedErrors("/documents/upload", formData, 30000);
// Invalidate list cache
invalidateETagCache("/api/knowledge-items");
invalidateETagCache("/api/knowledge-items/summary");
return result;
},
/**
* Stop a running crawl
*/
async stopCrawl(progressId: string): Promise<{ success: boolean; message: string }> {
return callKnowledgeAPI<{ success: boolean; message: string }>(`/api/knowledge-items/stop/${progressId}`, {
method: "POST",
});
},
/**
* Get document chunks for a knowledge item with pagination
*/
async getKnowledgeItemChunks(
sourceId: string,
options?: {
domainFilter?: string;
limit?: number;
offset?: number;
},
): Promise<ChunksResponse> {
const params = new URLSearchParams();
if (options?.domainFilter) {
params.append("domain_filter", options.domainFilter);
}
if (options?.limit !== undefined) {
params.append("limit", options.limit.toString());
}
if (options?.offset !== undefined) {
params.append("offset", options.offset.toString());
}
const queryString = params.toString();
const endpoint = `/api/knowledge-items/${sourceId}/chunks${queryString ? `?${queryString}` : ""}`;
return callKnowledgeAPI<ChunksResponse>(endpoint);
},
/**
* Get code examples for a knowledge item with pagination
*/
async getCodeExamples(
sourceId: string,
options?: {
limit?: number;
offset?: number;
},
): Promise<CodeExamplesResponse> {
const params = new URLSearchParams();
if (options?.limit !== undefined) {
params.append("limit", options.limit.toString());
}
if (options?.offset !== undefined) {
params.append("offset", options.offset.toString());
}
const queryString = params.toString();
const endpoint = `/api/knowledge-items/${sourceId}/code-examples${queryString ? `?${queryString}` : ""}`;
return callKnowledgeAPI<CodeExamplesResponse>(endpoint);
},
/**
* Search the knowledge base
*/
async searchKnowledgeBase(options: SearchOptions): Promise<SearchResultsResponse> {
return callKnowledgeAPI("/api/knowledge-items/search", {
method: "POST",
body: JSON.stringify(options),
});
},
/**
* Get available knowledge sources
*/
async getKnowledgeSources(): Promise<KnowledgeSource[]> {
return callKnowledgeAPI<KnowledgeSource[]>("/api/knowledge-items/sources");
},
};