|
import axios, { AxiosError } from 'axios' |
|
import { backendBaseUrl } from '@/lib/constants' |
|
import { errorMessage } from '@/lib/utils' |
|
import { useSettingsStore } from '@/stores/settings' |
|
import { navigationService } from '@/services/navigation' |
|
|
|
|
|
export type LightragNodeType = { |
|
id: string |
|
labels: string[] |
|
properties: Record<string, any> |
|
} |
|
|
|
export type LightragEdgeType = { |
|
id: string |
|
source: string |
|
target: string |
|
type: string |
|
properties: Record<string, any> |
|
} |
|
|
|
export type LightragGraphType = { |
|
nodes: LightragNodeType[] |
|
edges: LightragEdgeType[] |
|
} |
|
|
|
export type LightragStatus = { |
|
status: 'healthy' |
|
working_directory: string |
|
input_directory: string |
|
configuration: { |
|
llm_binding: string |
|
llm_binding_host: string |
|
llm_model: string |
|
embedding_binding: string |
|
embedding_binding_host: string |
|
embedding_model: string |
|
max_tokens: number |
|
kv_storage: string |
|
doc_status_storage: string |
|
graph_storage: string |
|
vector_storage: string |
|
workspace?: string |
|
max_graph_nodes?: string |
|
enable_rerank?: boolean |
|
rerank_model?: string | null |
|
rerank_binding_host?: string | null |
|
} |
|
update_status?: Record<string, any> |
|
core_version?: string |
|
api_version?: string |
|
auth_mode?: 'enabled' | 'disabled' |
|
pipeline_busy: boolean |
|
keyed_locks?: { |
|
process_id: number |
|
cleanup_performed: { |
|
mp_cleaned: number |
|
async_cleaned: number |
|
} |
|
current_status: { |
|
total_mp_locks: number |
|
pending_mp_cleanup: number |
|
total_async_locks: number |
|
pending_async_cleanup: number |
|
} |
|
} |
|
webui_title?: string |
|
webui_description?: string |
|
} |
|
|
|
export type LightragDocumentsScanProgress = { |
|
is_scanning: boolean |
|
current_file: string |
|
indexed_count: number |
|
total_files: number |
|
progress: number |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix' | 'bypass' |
|
|
|
export type Message = { |
|
role: 'user' | 'assistant' | 'system' |
|
content: string |
|
} |
|
|
|
export type QueryRequest = { |
|
query: string |
|
|
|
mode: QueryMode |
|
|
|
only_need_context?: boolean |
|
|
|
only_need_prompt?: boolean |
|
|
|
response_type?: string |
|
|
|
stream?: boolean |
|
|
|
top_k?: number |
|
|
|
chunk_top_k?: number |
|
|
|
max_entity_tokens?: number |
|
|
|
max_relation_tokens?: number |
|
|
|
max_total_tokens?: number |
|
|
|
|
|
|
|
|
|
conversation_history?: Message[] |
|
|
|
history_turns?: number |
|
|
|
user_prompt?: string |
|
|
|
enable_rerank?: boolean |
|
} |
|
|
|
export type QueryResponse = { |
|
response: string |
|
} |
|
|
|
export type DocActionResponse = { |
|
status: 'success' | 'partial_success' | 'failure' | 'duplicated' |
|
message: string |
|
} |
|
|
|
export type DeleteDocResponse = { |
|
status: 'deletion_started' | 'busy' | 'not_allowed' |
|
message: string |
|
doc_id: string |
|
} |
|
|
|
export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed' |
|
|
|
export type DocStatusResponse = { |
|
id: string |
|
content_summary: string |
|
content_length: number |
|
status: DocStatus |
|
created_at: string |
|
updated_at: string |
|
chunks_count?: number |
|
error?: string |
|
metadata?: Record<string, any> |
|
file_path: string |
|
} |
|
|
|
export type DocsStatusesResponse = { |
|
statuses: Record<DocStatus, DocStatusResponse[]> |
|
} |
|
|
|
export type AuthStatusResponse = { |
|
auth_configured: boolean |
|
access_token?: string |
|
token_type?: string |
|
auth_mode?: 'enabled' | 'disabled' |
|
message?: string |
|
core_version?: string |
|
api_version?: string |
|
webui_title?: string |
|
webui_description?: string |
|
} |
|
|
|
export type PipelineStatusResponse = { |
|
autoscanned: boolean |
|
busy: boolean |
|
job_name: string |
|
job_start?: string |
|
docs: number |
|
batchs: number |
|
cur_batch: number |
|
request_pending: boolean |
|
latest_message: string |
|
history_messages?: string[] |
|
update_status?: Record<string, any> |
|
} |
|
|
|
export type LoginResponse = { |
|
access_token: string |
|
token_type: string |
|
auth_mode?: 'enabled' | 'disabled' |
|
message?: string |
|
core_version?: string |
|
api_version?: string |
|
webui_title?: string |
|
webui_description?: string |
|
} |
|
|
|
export const InvalidApiKeyError = 'Invalid API Key' |
|
export const RequireApiKeError = 'API Key required' |
|
|
|
|
|
const axiosInstance = axios.create({ |
|
baseURL: backendBaseUrl, |
|
headers: { |
|
'Content-Type': 'application/json' |
|
} |
|
}) |
|
|
|
|
|
axiosInstance.interceptors.request.use((config) => { |
|
const apiKey = useSettingsStore.getState().apiKey |
|
const token = localStorage.getItem('LIGHTRAG-API-TOKEN'); |
|
|
|
|
|
if (token) { |
|
config.headers['Authorization'] = `Bearer ${token}` |
|
} |
|
if (apiKey) { |
|
config.headers['X-API-Key'] = apiKey |
|
} |
|
return config |
|
}) |
|
|
|
|
|
axiosInstance.interceptors.response.use( |
|
(response) => response, |
|
(error: AxiosError) => { |
|
if (error.response) { |
|
if (error.response?.status === 401) { |
|
|
|
if (error.config?.url?.includes('/login')) { |
|
throw error; |
|
} |
|
|
|
navigationService.navigateToLogin(); |
|
|
|
|
|
return Promise.reject(new Error('Authentication required')); |
|
} |
|
throw new Error( |
|
`${error.response.status} ${error.response.statusText}\n${JSON.stringify( |
|
error.response.data |
|
)}\n${error.config?.url}` |
|
) |
|
} |
|
throw error |
|
} |
|
) |
|
|
|
|
|
export const queryGraphs = async ( |
|
label: string, |
|
maxDepth: number, |
|
maxNodes: number |
|
): Promise<LightragGraphType> => { |
|
const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&max_nodes=${maxNodes}`) |
|
return response.data |
|
} |
|
|
|
export const getGraphLabels = async (): Promise<string[]> => { |
|
const response = await axiosInstance.get('/graph/label/list') |
|
return response.data |
|
} |
|
|
|
export const checkHealth = async (): Promise< |
|
LightragStatus | { status: 'error'; message: string } |
|
> => { |
|
try { |
|
const response = await axiosInstance.get('/health') |
|
return response.data |
|
} catch (error) { |
|
return { |
|
status: 'error', |
|
message: errorMessage(error) |
|
} |
|
} |
|
} |
|
|
|
export const getDocuments = async (): Promise<DocsStatusesResponse> => { |
|
const response = await axiosInstance.get('/documents') |
|
return response.data |
|
} |
|
|
|
export const scanNewDocuments = async (): Promise<{ status: string }> => { |
|
const response = await axiosInstance.post('/documents/scan') |
|
return response.data |
|
} |
|
|
|
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => { |
|
const response = await axiosInstance.get('/documents/scan-progress') |
|
return response.data |
|
} |
|
|
|
export const queryText = async (request: QueryRequest): Promise<QueryResponse> => { |
|
const response = await axiosInstance.post('/query', request) |
|
return response.data |
|
} |
|
|
|
export const queryTextStream = async ( |
|
request: QueryRequest, |
|
onChunk: (chunk: string) => void, |
|
onError?: (error: string) => void |
|
) => { |
|
const apiKey = useSettingsStore.getState().apiKey; |
|
const token = localStorage.getItem('LIGHTRAG-API-TOKEN'); |
|
const headers: HeadersInit = { |
|
'Content-Type': 'application/json', |
|
'Accept': 'application/x-ndjson', |
|
}; |
|
if (token) { |
|
headers['Authorization'] = `Bearer ${token}`; |
|
} |
|
if (apiKey) { |
|
headers['X-API-Key'] = apiKey; |
|
} |
|
|
|
try { |
|
const response = await fetch(`${backendBaseUrl}/query/stream`, { |
|
method: 'POST', |
|
headers: headers, |
|
body: JSON.stringify(request), |
|
}); |
|
|
|
if (!response.ok) { |
|
|
|
if (response.status === 401) { |
|
|
|
navigationService.navigateToLogin(); |
|
|
|
|
|
const authError = new Error('Authentication required'); |
|
throw authError; |
|
} |
|
|
|
|
|
let errorBody = 'Unknown error'; |
|
try { |
|
errorBody = await response.text(); |
|
} catch { } |
|
|
|
|
|
const url = `${backendBaseUrl}/query/stream`; |
|
throw new Error( |
|
`${response.status} ${response.statusText}\n${JSON.stringify( |
|
{ error: errorBody } |
|
)}\n${url}` |
|
); |
|
} |
|
|
|
if (!response.body) { |
|
throw new Error('Response body is null'); |
|
} |
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) { |
|
break; |
|
} |
|
|
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
|
|
|
|
const lines = buffer.split('\n'); |
|
buffer = lines.pop() || ''; |
|
|
|
for (const line of lines) { |
|
if (line.trim()) { |
|
try { |
|
const parsed = JSON.parse(line); |
|
if (parsed.response) { |
|
onChunk(parsed.response); |
|
} else if (parsed.error && onError) { |
|
onError(parsed.error); |
|
} |
|
} catch (error) { |
|
console.error('Error parsing stream chunk:', line, error); |
|
if (onError) onError(`Error parsing server response: ${line}`); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
if (buffer.trim()) { |
|
try { |
|
const parsed = JSON.parse(buffer); |
|
if (parsed.response) { |
|
onChunk(parsed.response); |
|
} else if (parsed.error && onError) { |
|
onError(parsed.error); |
|
} |
|
} catch (error) { |
|
console.error('Error parsing final chunk:', buffer, error); |
|
if (onError) onError(`Error parsing final server response: ${buffer}`); |
|
} |
|
} |
|
|
|
} catch (error) { |
|
const message = errorMessage(error); |
|
|
|
|
|
if (message === 'Authentication required') { |
|
|
|
console.error('Authentication required for stream request'); |
|
if (onError) { |
|
onError('Authentication required'); |
|
} |
|
return; |
|
} |
|
|
|
|
|
const statusCodeMatch = message.match(/^(\d{3})\s/); |
|
if (statusCodeMatch) { |
|
const statusCode = parseInt(statusCodeMatch[1], 10); |
|
|
|
|
|
let userMessage = message; |
|
|
|
switch (statusCode) { |
|
case 403: |
|
userMessage = 'You do not have permission to access this resource (403 Forbidden)'; |
|
console.error('Permission denied for stream request:', message); |
|
break; |
|
case 404: |
|
userMessage = 'The requested resource does not exist (404 Not Found)'; |
|
console.error('Resource not found for stream request:', message); |
|
break; |
|
case 429: |
|
userMessage = 'Too many requests, please try again later (429 Too Many Requests)'; |
|
console.error('Rate limited for stream request:', message); |
|
break; |
|
case 500: |
|
case 502: |
|
case 503: |
|
case 504: |
|
userMessage = `Server error, please try again later (${statusCode})`; |
|
console.error('Server error for stream request:', message); |
|
break; |
|
default: |
|
console.error('Stream request failed with status code:', statusCode, message); |
|
} |
|
|
|
if (onError) { |
|
onError(userMessage); |
|
} |
|
return; |
|
} |
|
|
|
|
|
if (message.includes('NetworkError') || |
|
message.includes('Failed to fetch') || |
|
message.includes('Network request failed')) { |
|
console.error('Network error for stream request:', message); |
|
if (onError) { |
|
onError('Network connection error, please check your internet connection'); |
|
} |
|
return; |
|
} |
|
|
|
|
|
if (message.includes('Error parsing') || message.includes('SyntaxError')) { |
|
console.error('JSON parsing error in stream:', message); |
|
if (onError) { |
|
onError('Error processing response data'); |
|
} |
|
return; |
|
} |
|
|
|
|
|
console.error('Unhandled stream error:', message); |
|
if (onError) { |
|
onError(message); |
|
} else { |
|
console.error('No error handler provided for stream error:', message); |
|
} |
|
} |
|
}; |
|
|
|
export const insertText = async (text: string): Promise<DocActionResponse> => { |
|
const response = await axiosInstance.post('/documents/text', { text }) |
|
return response.data |
|
} |
|
|
|
export const insertTexts = async (texts: string[]): Promise<DocActionResponse> => { |
|
const response = await axiosInstance.post('/documents/texts', { texts }) |
|
return response.data |
|
} |
|
|
|
export const uploadDocument = async ( |
|
file: File, |
|
onUploadProgress?: (percentCompleted: number) => void |
|
): Promise<DocActionResponse> => { |
|
const formData = new FormData() |
|
formData.append('file', file) |
|
|
|
const response = await axiosInstance.post('/documents/upload', formData, { |
|
headers: { |
|
'Content-Type': 'multipart/form-data' |
|
}, |
|
|
|
onUploadProgress: |
|
onUploadProgress !== undefined |
|
? (progressEvent) => { |
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total!) |
|
onUploadProgress(percentCompleted) |
|
} |
|
: undefined |
|
}) |
|
return response.data |
|
} |
|
|
|
export const batchUploadDocuments = async ( |
|
files: File[], |
|
onUploadProgress?: (fileName: string, percentCompleted: number) => void |
|
): Promise<DocActionResponse[]> => { |
|
return await Promise.all( |
|
files.map(async (file) => { |
|
return await uploadDocument(file, (percentCompleted) => { |
|
onUploadProgress?.(file.name, percentCompleted) |
|
}) |
|
}) |
|
) |
|
} |
|
|
|
export const clearDocuments = async (): Promise<DocActionResponse> => { |
|
const response = await axiosInstance.delete('/documents') |
|
return response.data |
|
} |
|
|
|
export const clearCache = async (modes?: string[]): Promise<{ |
|
status: 'success' | 'fail' |
|
message: string |
|
}> => { |
|
const response = await axiosInstance.post('/documents/clear_cache', { modes }) |
|
return response.data |
|
} |
|
|
|
export const deleteDocuments = async (docIds: string[], deleteFile: boolean = false): Promise<DeleteDocResponse> => { |
|
const response = await axiosInstance.delete('/documents/delete_document', { |
|
data: { doc_ids: docIds, delete_file: deleteFile } |
|
}) |
|
return response.data |
|
} |
|
|
|
export const getAuthStatus = async (): Promise<AuthStatusResponse> => { |
|
try { |
|
|
|
const response = await axiosInstance.get('/auth-status', { |
|
timeout: 5000, |
|
headers: { |
|
'Accept': 'application/json' |
|
} |
|
}); |
|
|
|
|
|
const contentType = response.headers['content-type'] || ''; |
|
if (contentType.includes('text/html')) { |
|
console.warn('Received HTML response instead of JSON for auth-status endpoint'); |
|
return { |
|
auth_configured: true, |
|
auth_mode: 'enabled' |
|
}; |
|
} |
|
|
|
|
|
if (response.data && |
|
typeof response.data === 'object' && |
|
'auth_configured' in response.data && |
|
typeof response.data.auth_configured === 'boolean') { |
|
|
|
|
|
if (!response.data.auth_configured) { |
|
if (response.data.access_token && typeof response.data.access_token === 'string') { |
|
return response.data; |
|
} else { |
|
console.warn('Auth not configured but no valid access token provided'); |
|
} |
|
} else { |
|
|
|
return response.data; |
|
} |
|
} |
|
|
|
|
|
console.warn('Received invalid auth status response:', response.data); |
|
|
|
|
|
return { |
|
auth_configured: true, |
|
auth_mode: 'enabled' |
|
}; |
|
} catch (error) { |
|
|
|
console.error('Failed to get auth status:', errorMessage(error)); |
|
return { |
|
auth_configured: true, |
|
auth_mode: 'enabled' |
|
}; |
|
} |
|
} |
|
|
|
export const getPipelineStatus = async (): Promise<PipelineStatusResponse> => { |
|
const response = await axiosInstance.get('/documents/pipeline_status') |
|
return response.data |
|
} |
|
|
|
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => { |
|
const formData = new FormData(); |
|
formData.append('username', username); |
|
formData.append('password', password); |
|
|
|
const response = await axiosInstance.post('/login', formData, { |
|
headers: { |
|
'Content-Type': 'multipart/form-data' |
|
} |
|
}); |
|
|
|
return response.data; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const updateEntity = async ( |
|
entityName: string, |
|
updatedData: Record<string, any>, |
|
allowRename: boolean = false |
|
): Promise<DocActionResponse> => { |
|
const response = await axiosInstance.post('/graph/entity/edit', { |
|
entity_name: entityName, |
|
updated_data: updatedData, |
|
allow_rename: allowRename |
|
}) |
|
return response.data |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const updateRelation = async ( |
|
sourceEntity: string, |
|
targetEntity: string, |
|
updatedData: Record<string, any> |
|
): Promise<DocActionResponse> => { |
|
const response = await axiosInstance.post('/graph/relation/edit', { |
|
source_id: sourceEntity, |
|
target_id: targetEntity, |
|
updated_data: updatedData |
|
}) |
|
return response.data |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const checkEntityNameExists = async (entityName: string): Promise<boolean> => { |
|
try { |
|
const response = await axiosInstance.get(`/graph/entity/exists?name=${encodeURIComponent(entityName)}`) |
|
return response.data.exists |
|
} catch (error) { |
|
console.error('Error checking entity name:', error) |
|
return false |
|
} |
|
} |
|
|