mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-31 20:00:22 -05:00
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>
224 lines
6.6 KiB
TypeScript
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");
|
|
},
|
|
};
|