|
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 |
|
} |
|
update_status?: Record<string, any> |
|
core_version?: string |
|
api_version?: string |
|
auth_mode?: 'enabled' | 'disabled' |
|
pipeline_busy: boolean |
|
} |
|
|
|
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' |
|
|
|
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 |
|
|
|
max_token_for_text_unit?: number |
|
|
|
max_token_for_global_context?: number |
|
|
|
max_token_for_local_context?: number |
|
|
|
hl_keywords?: string[] |
|
|
|
ll_keywords?: string[] |
|
|
|
|
|
|
|
|
|
conversation_history?: Message[] |
|
|
|
history_turns?: number |
|
} |
|
|
|
export type QueryResponse = { |
|
response: string |
|
} |
|
|
|
export type DocActionResponse = { |
|
status: 'success' | 'partial_success' | 'failure' | 'duplicated' |
|
message: 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 |
|
} |
|
|
|
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 |
|
} |
|
|
|
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, |
|
minDegree: number |
|
): Promise<LightragGraphType> => { |
|
const response = await axiosInstance.get(`/graphs?label=${encodeURIComponent(label)}&max_depth=${maxDepth}&min_degree=${minDegree}`) |
|
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 (e) { |
|
return { |
|
status: 'error', |
|
message: errorMessage(e) |
|
} |
|
} |
|
} |
|
|
|
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 |
|
) => { |
|
try { |
|
let buffer = '' |
|
await axiosInstance |
|
.post('/query/stream', request, { |
|
responseType: 'text', |
|
headers: { |
|
Accept: 'application/x-ndjson' |
|
}, |
|
transformResponse: [ |
|
(data: string) => { |
|
|
|
buffer += data |
|
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 (e) { |
|
console.error('Error parsing stream chunk:', e) |
|
if (onError) onError('Error parsing server response') |
|
} |
|
} |
|
} |
|
return data |
|
} |
|
] |
|
}) |
|
.catch((error) => { |
|
if (onError) onError(errorMessage(error)) |
|
}) |
|
|
|
|
|
if (buffer.trim()) { |
|
try { |
|
const parsed = JSON.parse(buffer) |
|
if (parsed.response) { |
|
onChunk(parsed.response) |
|
} else if (parsed.error && onError) { |
|
onError(parsed.error) |
|
} |
|
} catch (e) { |
|
console.error('Error parsing final chunk:', e) |
|
if (onError) onError('Error parsing server response') |
|
} |
|
} |
|
} catch (error) { |
|
const message = errorMessage(error) |
|
console.error('Stream request failed:', message) |
|
if (onError) onError(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 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; |
|
} |
|
|