Compare commits

...

170 Commits

Author SHA1 Message Date
yangdx 2f0aa7ed12 Optimize graph query by simplifying MATCH pattern
- Simplify MATCH clause to ()-[r]-()
- Remove node type constraints
- Improve query performance
2025-08-02 12:54:22 +08:00
Daniel.y a417f7c168
Merge pull request #1899 from danielaskdd/kv-storage-workspace
Fix: workspace isolation problem for json KV storage
2025-08-02 11:36:20 +08:00
yangdx e00690b41b Fix: workspace isolation problem for json KV storage
- Use workspace+namespace as final_namespace identifier
- Update all related storage operations
- Maintain backward compatibility
2025-08-02 11:30:19 +08:00
Daniel.y f6b90fe482
Merge pull request #1897 from danielaskdd/json-repair
refactor: improve JSON parsing reliability with json-repair library
2025-08-01 20:15:44 +08:00
yangdx d98fe6f340 Move json-repair to main dependencies 2025-08-01 19:59:39 +08:00
yangdx 32af45ff46 refactor: improve JSON parsing reliability with json-repair library
Replace regex-based JSON extraction with json-repair for better handling of malformed LLM responses. Remove deprecated JSON parsing utilities and clean up keyword_extraction parameter across LLM providers.

- Remove locate_json_string_body_from_string() and convert_response_to_json()
- Use json-repair.loads() in extract_keywords_only() for robust parsing
- Clean up LLM interfaces and remove unused parameters
- Add json-repair dependency
2025-08-01 19:36:20 +08:00
Daniel.y 3ae9c63b26
Merge pull request #1895 from danielaskdd/finalize-on-exit
Add graceful shutdown handling for LightRAG server
2025-08-01 12:12:40 +08:00
yangdx fdf051c234 Add graceful shutdown handling for LightRAG server
- Setup signal handlers for SIGINT/SIGTERM
- Clean up shared resources on shutdown
- Finalize shared data storage
2025-08-01 10:56:18 +08:00
yangdx 790abf148b Add psutil to API dependencies 2025-08-01 10:55:43 +08:00
yangdx 0f624b594a Update webui assets 2025-07-31 23:53:23 +08:00
yangdx 317d233486 Improve select dropdown styling in query settings 2025-07-31 23:52:44 +08:00
Daniel.y b7ec76e548
Merge pull request #1893 from danielaskdd/fix-core-depend-api
Fix: move OllamaServerInfos class to base module
2025-07-31 23:33:34 +08:00
yangdx 043e1376b3 Bump core version to v1.4.6 and api version to 0196 2025-07-31 23:27:31 +08:00
yangdx 8271e1f6f1 Move OllamaServerInfos class to base module
- Eliminate dependency of the core module on the API module.
2025-07-31 23:24:49 +08:00
Daniel.y 364ae2340d
Merge pull request #1891 from danielaskdd/fix-file-path-error
fix: Add safe handling for missing file_path in PostgreSQL
2025-07-31 18:11:52 +08:00
yangdx 9a8f58826d fix: Add safe handling for missing file_path and metadata in PostgreSQL doc status functions
- Add null-safe file_path handling with "no-file-path" fallback in get_docs_by_status and get_docs_by_track_id
- Enhance metadata validation to ensure dict type after JSON parsing
- Align PostgreSQL implementation with JSON implementation safety patterns
- Prevent KeyError exceptions when database records have missing fields
2025-07-31 18:07:53 +08:00
yangdx 9d5603d35e Set the default LLM temperature to 1.0 and centralize constant management 2025-07-31 17:15:10 +08:00
yangdx 3c530b21b6 Update README 2025-07-31 13:00:09 +08:00
Daniel.y 9b63a89054
Merge pull request #1889 from danielaskdd/query-param-ui
Refac: add query param reset buttons to webui
2025-07-31 12:38:47 +08:00
yangdx d3f1ea96de Update webui assets 2025-07-31 12:30:23 +08:00
yangdx a59f8e7ceb Add reset buttons to query settings controls
- Add reset functionality for all settings
- Include default values for each setting
2025-07-31 12:29:26 +08:00
yangdx c6bd9f0329 Disable conversation history by default
- Set default history_turns to 0
- Mark history_turns as deprecated
- Remove history_turns from example
- Update documentation comments
2025-07-31 12:28:42 +08:00
yangdx 755a08edec Remove History Turns config option and force value to 0
- Remove History Turns UI component from QuerySettings
- Update settings store version to 17 with migration
- Force history_turns parameter to always be 0 in queries
- Prevent future modifications to history_turns setting
2025-07-31 11:22:48 +08:00
yangdx 5b8989e4d9 Update webui assets 2025-07-31 03:47:35 +08:00
yangdx 47002e645b fix: resolve quick refresh after clearing documents in DocumentManager
- Fix handleDocumentsCleared to use proper 30s idle interval instead of 500ms
- Update smart polling useEffect to depend on complete statusCounts object
- Reset status counts immediately when documents are cleared
2025-07-31 03:45:11 +08:00
Daniel.y 5e2b262094
Merge pull request #1886 from danielaskdd/doc-list-paging
Feat: add document list pagination for webui
2025-07-31 03:05:54 +08:00
yangdx 41de51a4db fix: add missing await in MongoDB get_all_status_counts aggregation
Resolves 'coroutine' object has no attribute 'to_list' error in document pagination endpoint by adding missing await keyword before self._data.aggregate() call.
2025-07-31 02:27:16 +08:00
yangdx 2af8a93dc7 fix: resolve _sort_key error in Redis get_docs_paginated function 2025-07-31 02:16:56 +08:00
yangdx d0bc5e7c4a Extend path filter to also cover POST requests 2025-07-31 02:06:56 +08:00
yangdx 5282312c64 Update webui assets 2025-07-31 02:01:50 +08:00
yangdx 78b4ff2d0b Bump api version to 0195 2025-07-31 02:00:26 +08:00
yangdx 3e5efd0b27 Add /documents/paginated to filtered logging paths 2025-07-31 02:00:00 +08:00
yangdx a8f7e125c4 Update i18n translation 2025-07-31 01:59:26 +08:00
yangdx 45f27fccc3 feat(webui): Implement intelligent polling and responsive health checks
- Relocate the health check functionality from aap.tsx to state.ts to enable initialization by other components.
- Replaced the fixed 5-second polling with a dynamic interval. The polling interval is extended to 30 seconds when the no files in pending an processing state.
- Data refresh is triggered instantly when  `pipelineBusy` state changed
- Health check is triggered  after clicking "Scan New Documents" or "Clear Documents"
2025-07-31 01:37:24 +08:00
yangdx 93dede163d feat: move document list reset button to right side with ghost style 2025-07-30 22:37:22 +08:00
yangdx 08da46ac0f Bump api version to 0194 2025-07-30 18:50:53 +08:00
yangdx 83dc672f21 Update webui assets 2025-07-30 18:50:22 +08:00
yangdx 0eac1a883a Feat: add file path sorting for document manager
- Add file_path sorting support to all database backends (JSON, Redis, PostgreSQL, MongoDB)
- Implement smart column header switching between "ID" and "File Name" based on display mode
- Add automatic sort field switching when toggling between ID and file name display
- Create composite indexes for workspace+file_path in PostgreSQL and MongoDB for better query performance
- Update frontend to maintain sort state when switching display modes
- Add internationalization support for "fileName" in English and Chinese locales

This enhancement improves user experience by providing intuitive file-based sorting
while maintaining performance through optimized database indexes.
2025-07-30 18:46:55 +08:00
yangdx e60c26ea77 feat: set default page size to 10 and persist in localStorage
- Change default documents page size from 20 to 10 rows
- Add documentsPageSize setting to settings store with persistence
- Update DocumentManager to use and save page size preference
- Include migration for existing users to get new default value
2025-07-30 18:07:38 +08:00
yangdx 74eecc46e5 feat(pagination): Implement document list pagination backends and frontend UI
- Add pagination support to BaseDocStatusStorage interface and all implementations (PostgreSQL, MongoDB, Redis, JSON)
- Implement RESTful API endpoints for paginated document queries and status counts
- Create reusable pagination UI components with internationalization support
- Optimize performance with database-level pagination and efficient in-memory processing
- Maintain backward compatibility while adding configurable page sizes (10-200 items)
2025-07-30 17:58:32 +08:00
yangdx c24c2ff2f6 Remove deprecated temp file saving function
- Delete unused save_temp_file function
2025-07-30 14:23:08 +08:00
yangdx b456bb0942 Fix linting 2025-07-30 13:59:50 +08:00
yangdx a788899285 Fix linting 2025-07-30 13:32:15 +08:00
yangdx 797dcc1ff1 Update README 2025-07-30 13:31:47 +08:00
yangdx 50621d5a94 Update docs 2025-07-30 13:16:26 +08:00
yangdx aba46213a7 Update README 2025-07-30 13:13:59 +08:00
yangdx c7bc4fc42c Add track_id return to document processing pipeline 2025-07-30 10:27:12 +08:00
Daniel.y 444593bda8
Merge pull request #1878 from Ja1aia/main
fix timeout issue
2025-07-30 09:19:46 +08:00
Daniel.y 6d0a644844
Merge pull request #1883 from danielaskdd/add-metadata
feat: add processing time tracking to document status with metadata field
2025-07-30 09:15:36 +08:00
Daniel.y 1c35322088
Merge pull request #1882 from danielaskdd/add-track-id-support
Feat: add Document Processing Track ID Support for Frontend
2025-07-30 09:15:21 +08:00
Daniel.y 6fa5a6f634
Merge pull request #1881 from danielaskdd/remove-content-in-doc-status
Refact remove content in doc status to improve performance
2025-07-30 09:15:06 +08:00
yangdx 29e829113b Fix status key serialization issue in get_rack_status 2025-07-30 04:45:48 +08:00
yangdx 30f71c8acf Remove _id field and improve index handling in MongoDB
- Remove MongoDB _id field from documents
- Improve index existence check and creation
2025-07-30 04:17:26 +08:00
yangdx cfb7117dd6 Fix track_id missing for query in PostgreSQL 2025-07-30 03:44:20 +08:00
yangdx 5ec7eedf37 Bump api version to 0193 2025-07-30 03:11:44 +08:00
yangdx faa59cac72 Update webui assets 2025-07-30 03:11:19 +08:00
yangdx cbaede8455 Add ScanResponse type for scan endpoint in webui 2025-07-30 03:11:09 +08:00
yangdx 7207598fc4 Fix track_id bugs and add track_id to scanning response 2025-07-30 03:06:20 +08:00
yangdx 75de799353 Remove deprecated content field from doc status storage
- Remove content field from JSON storage
- Remove content field from MongoDB storage
- Remove content field from Redis storage
2025-07-30 01:00:06 +08:00
yangdx 3ef3b8e155 Update webui assets 2025-07-30 00:06:27 +08:00
yangdx 6f958d5aee feat: add metadata timestamps to document processing and update frontend compatibility
- Add metadata field to doc_status storage with Unix timestamps for processing start/end times
- Update frontend API types: error -> error_msg, add track_id and metadata support
- Add getTrackStatus API method for document tracking functionality
- Fix frontend DocumentManager to use error_msg field for proper error display
- Ensure full compatibility between backend metadata changes and frontend UI
2025-07-30 00:04:27 +08:00
yangdx 93afa7d8a7 feat: add processing time tracking to document status with metadata field
- Add metadata field to DocProcessingStatus with start_time and end_time tracking
- Record processing timestamps using Unix time format (seconds precision)
- Update all storage backends (JSON, MongoDB, Redis, PostgreSQL) for new field support
- Maintain backward compatibility with default values for existing data
- Add error_msg field for better error tracking during document processing
2025-07-29 23:42:33 +08:00
yangdx 7206c07468 Remove deprecated content field from doc status
- Drop content column from LIGHTRAG_DOC_STATUS
- Clean up doc status handling code
- Maintain backward compatibility
2025-07-29 23:19:36 +08:00
yangdx 1e1adcb64a Add index on track_id column in doc status table of PostgreSQL 2025-07-29 23:03:09 +08:00
yangdx 6014b9bf73 feat: add track_id support for document processing progress monitoring
- Add get_docs_by_track_id() method to all storage backends (MongoDB, PostgreSQL, Redis, JSON)
- Implement automatic track_id generation with upload_/insert_ prefixes
- Add /track_status/{track_id} API endpoint for frontend progress queries
- Create database indexes for efficient track_id lookups
- Enable real-time document processing status tracking across all storage types
2025-07-29 22:24:21 +08:00
yangdx dafdf92715 Remove content fallback logic in get_docs_by_status from Redis 2025-07-29 19:13:07 +08:00
yangdx 40a4cacee0 Merge branch 'main' into remove-content-from-doc-status 2025-07-29 16:15:01 +08:00
yangdx 92bbb7a1b3 Remove content fallback and standardize doc status handling
- Remove content_summary fallback logic
- Standardize doc status processing
- Handle missing file_path consistently
2025-07-29 16:13:51 +08:00
yangdx 24c36d876c Remove content field from DocProcessingStatus, update MongoDB and PostgreSQL implementation 2025-07-29 14:52:45 +08:00
administrator 9c3e1505b5 fix timeout issue 2025-07-29 13:38:46 +07:00
yangdx 8274ed52d1 feat: separate document content from doc_status to improve performance
This optimization significantly improves doc_status query/update performance by avoiding large string operations during frequent status checks.
2025-07-29 14:20:07 +08:00
Daniel.y c4544c23a7
Merge pull request #1876 from Ja1aia/main
Fix: corrected unterminated f-string in config.py
2025-07-29 14:12:57 +08:00
administrator c26dfa33de Fix: corrected unterminated f-string in config.py 2025-07-29 11:21:23 +07:00
Daniel.y 7a5df185a5
Merge pull request #1875 from danielaskdd/remove-embedding-max-token-size
refactor: Remove deprecated `max_token_size` from embedding configura…
2025-07-29 11:23:54 +08:00
yangdx 9923821d75 refactor: Remove deprecated `max_token_size` from embedding configuration
This parameter is no longer used. Its removal simplifies the API and clarifies that token length management is handled by upstream text chunking logic rather than the embedding wrapper.
2025-07-29 10:49:35 +08:00
yangdx d26d413d97 Merge branch 'patch-1' 2025-07-29 09:57:58 +08:00
yangdx f4c2dc327d Fix linting 2025-07-29 09:57:41 +08:00
yangdx 75d1b1e9f8 Update Ollama context length configuration
- Rename OLLAMA_NUM_CTX to OLLAMA_LLM_NUM_CTX
- Increase default context window size
- Add requirement for minimum context size
- Update documentation examples
2025-07-29 09:53:37 +08:00
yangdx 645f81f7c8 fixes a critical bug where Ollama options were not being applied correctly
`dict.update()` modifies the dictionary in-place and returns `None`.
2025-07-29 09:52:25 +08:00
yangdx 9bdbdae120 Update README 2025-07-29 08:22:48 +08:00
Michele Comitini bd94714b15 options needs to be passed to ollama client embed() method
Fix line length

Create binding_options.py

Remove test property

Add dynamic binding options to CLI and environment config

Automatically generate command-line arguments and environment variable
support for all LLM provider bindings using BindingOptions. Add sample
.env generation and extensible framework for new providers.

Add example option definitions and fix test arg check in OllamaOptions

Add options_dict method to BindingOptions for argument parsing

Add comprehensive Ollama binding configuration options

ruff formatting Apply ruff formatting to binding_options.py

Add Ollama separate options for embedding and LLM

Refactor Ollama binding options and fix class var handling

The changes improve how class variables are handled in binding options
and better organize the Ollama-specific options into LLM and embedding
subclasses.

Fix typo in arg test.

Rename cls parameter to klass to avoid keyword shadowing

Fix Ollama embedding binding name typo

Fix ollama embedder context param name

Split Ollama options into LLM and embedding configs with mixin base

Add Ollama option configuration to LLM and embeddings in lightrag_server

Update sample .env generation and environment handling

Conditionally add env vars and cmdline options only when ollama bindings
are used. Add example env file for Ollama binding options.
2025-07-28 12:05:40 +02:00
yangdx ee53e43568 Update webui assets 2025-07-28 02:52:32 +08:00
yangdx 84b09aa5da feat: add threshold status line to StatusCard with i18n support
- Add cosine_threshold, min_rerank_score, related_chunk_number to LightragStatus type
2025-07-28 02:51:36 +08:00
Daniel.y 1613f4410e
Merge pull request #1873 from danielaskdd/add-status
Feat(webui): enhance status card with new settings from health endpoint
2025-07-28 02:34:44 +08:00
yangdx 769f77ef8f Update webui assets 2025-07-28 02:26:07 +08:00
yangdx 958ed80b66 Update translation strings for document processing
- Renamed "maxParallelInsert" to be more descriptive
- Removed "maxTokens" from all translations
2025-07-28 02:24:41 +08:00
yangdx 5aceca0052 feat(webui): enhance status card with new health endpoint data
- Update StatusCard to display consolidated server info with parallel insert limits and summary settings
- Merge LLM and embedding configurations with async parameters for cleaner display
- Add new status fields to TypeScript interface (summary_language, max_parallel_insert, etc.)
2025-07-28 02:19:27 +08:00
yangdx 98ac6fb3f0 Bump api version to 0192 2025-07-28 01:42:51 +08:00
yangdx f2ffff063b feat: refactor ollama server configuration management
- Add ollama_server_infos attribute to LightRAG class with default initialization
- Move default values to constants.py for centralized configuration
- Refactor OllamaServerInfos class with property accessors and CLI support
- Update OllamaAPI to get configuration through rag object instead of direct import
- Add command line arguments for simulated model name and tag
- Fix type imports to avoid circular dependencies
2025-07-28 01:38:35 +08:00
yangdx 598eecd06d Refactor: Rename llm_model_max_token_size to summary_max_tokens
This commit renames the parameter 'llm_model_max_token_size' to 'summary_max_tokens' for better clarity, as it specifically controls the token limit for entity relation summaries.
2025-07-28 00:49:08 +08:00
yangdx d0d57a45b6 feat: add environment variables to /health endpoint and centralize defaults
- Add 9 environment variables to /health endpoint configuration section
- Centralize default constants in lightrag/constants.py for consistency
- Update config.py to use centralized defaults for better maintainability
2025-07-28 00:30:56 +08:00
yangdx 9c4e98ec3b Unify entity extraction prompt between passes
- Disallow hallucinated info in descriptions
- Align reminder steps with main extraction
2025-07-27 23:06:55 +08:00
Daniel.y 4eef9f3778
Merge pull request #1845 from AkosLukacs/patch-2
Better prompt for entity description extraction to avoid hallucinations
2025-07-27 22:38:08 +08:00
yangdx 3951a44666 Revert file_path build method, built from related chunks 2025-07-27 21:56:20 +08:00
yangdx d70c584d80 Bump api version to 0191 2025-07-27 21:24:53 +08:00
Daniel.y 7da485dd40
Merge pull request #1872 from danielaskdd/main
Fix: Improve keyword extraction prompt for robust JSON output.
2025-07-27 21:21:47 +08:00
yangdx 35734baa5c Merge remote-tracking branch 'upstream/main' 2025-07-27 21:12:29 +08:00
yangdx f2d051eea5 Fix: Improve keyword extraction prompt for robust JSON output.
*   Emphasize strict JSON output in key extration prompt
*   Clean up prompt examples in key extration prompt
*   Log raw LLM response on JSON error
2025-07-27 21:10:47 +08:00
yangdx 519e81aaeb Update env.example 2025-07-27 18:21:10 +08:00
yangdx 3f5ade47cd Update README 2025-07-27 17:26:49 +08:00
Daniel.y 2054bba7e6
Merge pull request #1871 from danielaskdd/min-rank-score
feat: Add rerank score filtering with configurable threshold
2025-07-27 17:01:22 +08:00
yangdx e09929b42e Refine rerank filtering log message for clarity 2025-07-27 16:57:38 +08:00
yangdx f4bca7bfb2 Fix linting 2025-07-27 16:50:45 +08:00
yangdx a9565d7379 feat: Skip rerank filtering when `min_rerank_score` is 0.0 2025-07-27 16:50:12 +08:00
yangdx ebaff228aa feat: Add rerank score filtering with configurable threshold
- Add DEFAULT_MIN_RERANK_SCORE constant (default: 0.0)
- Add MIN_RERANK_SCORE environment variable support
- Filter chunks with rerank scores below threshold in process_chunks_unified
- Add info-level logging for filtering operations
- Handle empty results gracefully after filtering
- Maintain backward compatibility with non-reranked chunks
2025-07-27 16:37:44 +08:00
Daniel.y 358fbd689f
Merge pull request #1869 from danielaskdd/file_paths
refactor: unify file_path handling across merge and rebuild functions
2025-07-27 12:38:44 +08:00
yangdx 99e3812c38 refactor: unify file_path handling across merge and rebuild functions
- Replace simple string concatenation with build_file_path() in:
  - _merge_edges_then_upsert
  - _rebuild_single_entity
  - _rebuild_single_relationship
- Ensures consistent deduplication, length limiting, and error handling
- Aligns with existing _merge_nodes_then_upsert implementation
2025-07-27 12:37:24 +08:00
Daniel.y c6cfbee3e8
Merge pull request #1868 from danielaskdd/optimize-prompt
Refine entity continuation prompt to avoid duplicates.
2025-07-27 10:49:39 +08:00
yangdx cf1ca39b3f Refine entity continuation prompt to avoid duplicates.
- Clarify finding missing entities
- Instruct not to repeat extractions
2025-07-27 10:48:29 +08:00
yangdx 0dfbce0bb4 Update the README to clarify the explanation of concurrent processes. 2025-07-27 10:39:28 +08:00
yangdx 055629d30d Reduce default max total tokens to 30k 2025-07-27 10:33:06 +08:00
yangdx a67f93acc9 Replace hardcoded max tokens with DEFAULT_MAX_TOTAL_TOKENS constant
- Use constant in process_chunks_unified
- Update WebUI default to match (32000)
2025-07-26 11:23:54 +08:00
yangdx 7b915b34f6 Refactor: move build_file_path function from operate.py to utils.py 2025-07-26 10:52:59 +08:00
yangdx c8c3545454 refactor: extract file path length limit to shared constant
• Add DEFAULT_MAX_FILE_PATH_LENGTH constant
• Replace hardcoded 4090 in Milvus impl
2025-07-26 10:45:03 +08:00
yangdx 8e7014d366 Merge branch 'separator_file_path' 2025-07-26 10:39:03 +08:00
yangdx a943265257 fix: preserve file path order in build_file_path function 2025-07-26 10:21:32 +08:00
yangdx 6efa8ab263 Improve file path length warning message clarity and urgency
• Change debug to warning level
• Simplify message wording
2025-07-26 10:00:18 +08:00
Daniel.y 2ed046171e
Merge pull request #1863 from HKUDS/fix-sigma-null-blendFunc
fix(webui): Correct edge renderer for sigma.js v3
2025-07-26 08:47:47 +08:00
yangdx e7baf54ec2 Update webui assets 2025-07-26 08:43:12 +08:00
yangdx 2c4f621ded fix(webui): Correct edge renderer for sigma.js v3
The `curvedNoArrow` edge type was incorrectly configured, causing a `TypeError` during graph rendering. This commit updates the `edgeProgramClasses` in `GraphViewer.tsx` to use the `createEdgeCurveProgram()` factory function as required by `@sigma/edge-curve` v3, resolving the crash.
2025-07-26 08:42:19 +08:00
xuewei 56c3cb2dbe Improve build_file_path log 2025-07-26 08:38:02 +08:00
yangdx 10b55dbdc7 Revert "Add no-cache headers to index.html via custom Vite plugin"
This reverts commit 93ce53394f.
2025-07-26 08:28:05 +08:00
okxuewei 912fc0fc31
Merge branch 'HKUDS:main' into separator_file_path 2025-07-26 08:17:35 +08:00
yangdx b3c2987006 Reduce default MAX_TOKENS from 32000 to 10000 2025-07-26 08:13:49 +08:00
xuewei a49b7758e1 Merge branch 'main' of https://github.com/okxuewei/LightRAG into separator_file_path 2025-07-26 00:47:48 +08:00
xuewei b4da3de7d9 Improve file_path drop policy 2025-07-26 00:46:02 +08:00
xuewei 55e2678a1e Improve file_path FieldSchema 4090 2025-07-26 00:22:25 +08:00
yangdx 6a99d7ac28 Update webui assets 2025-07-25 22:03:58 +08:00
yangdx 93ce53394f Add no-cache headers to index.html via custom Vite plugin
• Create noCachePlugin for Vite
• Add cache-control meta tags
• Inject headers into index.html
• Prevent browser caching issues
2025-07-25 22:03:25 +08:00
yangdx 4ae44bb24b Bump core version to 1.4.5 and api version to 0190 2025-07-25 11:15:04 +08:00
yangdx ccfe2e73d1 Update env.example 2025-07-25 11:13:15 +08:00
yangdx 983bacd87e Update logger messages 2025-07-24 16:49:28 +08:00
yangdx bf58c73c3f Update webui assets 2025-07-24 16:48:45 +08:00
yangdx 33d986aa2a Change default history_turns from 3 to 0 in settings store of webui 2025-07-24 16:47:54 +08:00
yangdx 44b7ce222e feat: add default storage dependencies and optimize imports
- Add nano-vectordb and networkx to pyproject.toml dependencies
- Replace dynamic imports with direct imports for 4 default storage implementations
- Improve startup performance while maintaining backward compatibility
2025-07-24 16:14:26 +08:00
yangdx 51231c7647 Update README 2025-07-24 15:48:49 +08:00
Daniel.y 733adb420d
Merge pull request #1848 from HKUDS/context-builder
Refac: optimizing context builder to improve query performance
2025-07-24 15:32:28 +08:00
zrguo 55ddc0ee86 fix typo 2025-07-24 15:20:05 +08:00
yangdx 29d1220f26 Merge branch 'main' into context-builder 2025-07-24 14:18:12 +08:00
yangdx b1dd015e3e Update env.example 2025-07-24 14:17:49 +08:00
yangdx 5437509824 Bump api version to 0189 2025-07-24 14:07:48 +08:00
yangdx f57ed21593 Merge branch 'main' into context-builder 2025-07-24 14:07:05 +08:00
Daniel.y ac7d8560bd
Merge pull request #1852 from danielaskdd/fix-postgres-ssl
fix(postgres): handle ssl_mode="allow" in _create_ssl_context
2025-07-24 12:54:05 +08:00
yangdx 5574a30856 fix(postgres): handle ssl_mode="allow" in _create_ssl_context
Add "allow" to the list of recognized SSL modes in PostgreSQL connection helper. Previously, ssl_mode="allow" would fall through to "Unknown SSL mode" warning. Now it's properly handled alongside "require" and "prefer" modes.
2025-07-24 12:45:13 +08:00
Daniel.y 2e0a9dc73b
Merge pull request #1851 from danielaskdd/add-jina-support
feat: Integrate Jina embeddings API support
2025-07-24 12:28:40 +08:00
yangdx d8e7b77099 Bump api version to 0188 2025-07-24 12:27:30 +08:00
yangdx 2767212ba0 Fix linting 2025-07-24 12:25:50 +08:00
yangdx d979e9078f feat: Integrate Jina embeddings API support
- Implemented Jina embedding function
- Add new EMBEDDING_BINDING type of jina for LightRAG Server
- Add env var sample
2025-07-24 12:15:00 +08:00
yangdx 4f8de4ecdb Merge branch 'main' into context-builder 2025-07-24 10:51:59 +08:00
yangdx edeb89abc6 Update comments 2025-07-24 10:09:24 +08:00
yangdx 795ce4cbe7 Update RELATED_CHUNK_NUMBER comment 2025-07-24 10:06:33 +08:00
yangdx d78fda1d89 Optimize logger message 2025-07-24 04:31:06 +08:00
yangdx baad473b67 Bump api version to 0188 2025-07-24 04:00:43 +08:00
yangdx d97913873b Update logger message 2025-07-24 03:44:02 +08:00
yangdx 3075691f72 Refactor: move reranking utilities from operate.py to utils.py
• Move apply_rerank_if_enabled to utils
• Move process_chunks_unified to utils
2025-07-24 03:33:38 +08:00
yangdx 2c940f0728 reduce RELATED_CHUNK_NUMBER from 10 to 5 2025-07-24 02:49:05 +08:00
yangdx 5a5d32dc32 Optimize logger message 2025-07-24 02:13:39 +08:00
yangdx 42710221f5 Update log messages 2025-07-24 01:31:49 +08:00
yangdx 02f79508e0 Optimize context building with weighted polling and round-robin data selection 2025-07-24 01:18:21 +08:00
Daniel.y 0009a37efb
Merge pull request #1844 from AkosLukacs/patch-1
Fix "A Simple Program" example in README.md
2025-07-23 18:08:45 +08:00
yangdx 958151e610 Merge branch 'main' into context-builder 2025-07-23 16:17:11 +08:00
yangdx 7d96ca98f7 Fix linting 2025-07-23 16:16:37 +08:00
yangdx 00d7bc80bf Merge branch 'context-builder' 2025-07-23 16:14:44 +08:00
yangdx 75fd6c73ea Merge branch 'main' into context-builder 2025-07-23 16:14:14 +08:00
yangdx 6cc9411c86 fix: handle empty tasks list in merge_nodes_and_edges to prevent ValueError
- Add empty tasks check before calling asyncio.wait()
- Return early with logging when no entities/relationships to process
2025-07-23 16:06:47 +08:00
yangdx 2d41e5313a Remove redundant tokenizer checks 2025-07-23 10:19:45 +08:00
Ákos Lukács 75beaf249e
Better prompt for entity description extraction to avoid hallucinations 2025-07-22 20:40:46 +02:00
Ákos Lukács f115661e16
Fix "A Simple Program" example in README.md
The example should use ainsert and aquery. Fixes #1723
2025-07-22 14:37:15 +02:00
zrguo 681d43bb32 fix typo 2025-07-22 15:34:51 +08:00
yangdx ce9dac9bcf vdb does not store rank any more 2025-07-21 17:04:23 +08:00
111 changed files with 5177 additions and 1803 deletions

View File

@ -265,14 +265,13 @@ if __name__ == "__main__":
| **embedding_func_max_async** | `int` | 最大并发异步嵌入进程数 | `16` |
| **llm_model_func** | `callable` | LLM生成的函数 | `gpt_4o_mini_complete` |
| **llm_model_name** | `str` | 用于生成的LLM模型名称 | `meta-llama/Llama-3.2-1B-Instruct` |
| **llm_model_max_token_size** | `int` | 生成实体关系摘要时送给LLM的最大令牌数 | `32000`默认值由环境变量MAX_TOKENS更改 |
| **summary_max_tokens** | `int` | 生成实体关系摘要时送给LLM的最大令牌数 | `32000`默认值由环境变量MAX_TOKENS更改 |
| **llm_model_max_async** | `int` | 最大并发异步LLM进程数 | `4`默认值由环境变量MAX_ASYNC更改 |
| **llm_model_kwargs** | `dict` | LLM生成的附加参数 | |
| **vector_db_storage_cls_kwargs** | `dict` | 向量数据库的附加参数,如设置节点和关系检索的阈值 | cosine_better_than_threshold: 0.2默认值由环境变量COSINE_THRESHOLD更改 |
| **enable_llm_cache** | `bool` | 如果为`TRUE`将LLM结果存储在缓存中重复的提示返回缓存的响应 | `TRUE` |
| **enable_llm_cache_for_entity_extract** | `bool` | 如果为`TRUE`将实体提取的LLM结果存储在缓存中适合初学者调试应用程序 | `TRUE` |
| **addon_params** | `dict` | 附加参数,例如`{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"]}`:设置示例限制、输出语言和文档处理的批量大小 | `example_number: 所有示例, language: English` |
| **convert_response_to_json_func** | `callable` | 未使用 | `convert_response_to_json` |
| **embedding_cache_config** | `dict` | 问答缓存的配置。包含三个参数:`enabled`:布尔值,启用/禁用缓存查找功能。启用时,系统将在生成新答案之前检查缓存的响应。`similarity_threshold`浮点值0-1相似度阈值。当新问题与缓存问题的相似度超过此阈值时将直接返回缓存的答案而不调用LLM。`use_llm_check`:布尔值,启用/禁用LLM相似度验证。启用时在返回缓存答案之前将使用LLM作为二次检查来验证问题之间的相似度。 | 默认:`{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
</details>
@ -320,7 +319,7 @@ class QueryParam:
max_relation_tokens: int = int(os.getenv("MAX_RELATION_TOKENS", "10000"))
"""Maximum number of tokens allocated for relationship context in unified token control system."""
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "32000"))
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "30000"))
"""Maximum total tokens budget for the entire query context (entities + relations + chunks + system prompt)."""
hl_keywords: list[str] = field(default_factory=list)
@ -334,7 +333,8 @@ class QueryParam:
Format: [{"role": "user/assistant", "content": "message"}].
"""
history_turns: int = 3
# Deprated: history message have negtive effect on query performance
history_turns: int = 0
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
ids: list[str] | None = None
@ -396,7 +396,6 @@ async def initialize_rag():
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=4096,
max_token_size=8192,
func=embedding_func
)
)
@ -425,7 +424,6 @@ rag = LightRAG(
# 使用Hugging Face嵌入函数
embedding_func=EmbeddingFunc(
embedding_dim=384,
max_token_size=5000,
func=lambda texts: hf_embed(
texts,
tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2"),
@ -452,7 +450,6 @@ rag = LightRAG(
# 使用Ollama嵌入函数
embedding_func=EmbeddingFunc(
embedding_dim=768,
max_token_size=8192,
func=lambda texts: ollama_embed(
texts,
embed_model="nomic-embed-text"
@ -504,7 +501,6 @@ rag = LightRAG(
# 使用Ollama嵌入函数
embedding_func=EmbeddingFunc(
embedding_dim=768,
max_token_size=8192,
func=lambda texts: ollama_embed(
texts,
embed_model="nomic-embed-text"
@ -547,7 +543,6 @@ async def initialize_rag():
llm_model_func=llama_index_complete_if_cache, # LlamaIndex兼容的完成函数
embedding_func=EmbeddingFunc( # LlamaIndex兼容的嵌入函数
embedding_dim=1536,
max_token_size=8192,
func=lambda texts: llama_index_embed(texts, embed_model=embed_model)
),
)
@ -690,7 +685,7 @@ rag.insert(["文本1", "文本2",...], ids=["文本1的ID", "文本2的ID"])
</details>
<details>
<summary><b>使用管道插入</b></summary>
<summary><b>使用流水线插入</b></summary>
`apipeline_enqueue_documents`和`apipeline_process_enqueue_documents`函数允许您对文档进行增量插入到图中。
@ -786,7 +781,6 @@ async def initialize_rag():
<summary> <b>使用Faiss进行存储</b> </summary>
在使用Faiss向量数据库之前必须手工安装`faiss-cpu`或`faiss-gpu`。
- 安装所需依赖:
```
@ -809,7 +803,6 @@ rag = LightRAG(
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=384,
max_token_size=8192,
func=embedding_func,
),
vector_storage="FaissVectorDBStorage",
@ -824,51 +817,13 @@ rag = LightRAG(
<details>
<summary> <b>使用PostgreSQL进行存储</b> </summary>
对于生产级场景您很可能想要利用企业级解决方案。PostgreSQL可以为您提供一站式解决方案作为KV存储、向量数据库pgvector和图数据库apache AGE
对于生产级场景您很可能想要利用企业级解决方案。PostgreSQL可以为您提供一站式解决方案作为KV存储、向量数据库pgvector和图数据库apache AGE支持 PostgreSQL 版本为16.6或以上。
* PostgreSQL很轻量整个二进制发行版包括所有必要的插件可以压缩到40MB参考[Windows发布版](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0)它在Linux/Mac上也很容易安装。
* 如果您是初学者并想避免麻烦推荐使用docker请从这个镜像开始请务必阅读概述https://hub.docker.com/r/shangor/postgres-for-rag
* 如何开始?参考:[examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
* 为AGE创建索引示例如有必要将下面的`dickens`改为您的图名)
```sql
load 'age';
SET search_path = ag_catalog, "$user", public;
CREATE INDEX CONCURRENTLY entity_p_idx ON dickens."Entity" (id);
CREATE INDEX CONCURRENTLY vertex_p_idx ON dickens."_ag_label_vertex" (id);
CREATE INDEX CONCURRENTLY directed_p_idx ON dickens."DIRECTED" (id);
CREATE INDEX CONCURRENTLY directed_eid_idx ON dickens."DIRECTED" (end_id);
CREATE INDEX CONCURRENTLY directed_sid_idx ON dickens."DIRECTED" (start_id);
CREATE INDEX CONCURRENTLY directed_seid_idx ON dickens."DIRECTED" (start_id,end_id);
CREATE INDEX CONCURRENTLY edge_p_idx ON dickens."_ag_label_edge" (id);
CREATE INDEX CONCURRENTLY edge_sid_idx ON dickens."_ag_label_edge" (start_id);
CREATE INDEX CONCURRENTLY edge_eid_idx ON dickens."_ag_label_edge" (end_id);
CREATE INDEX CONCURRENTLY edge_seid_idx ON dickens."_ag_label_edge" (start_id,end_id);
create INDEX CONCURRENTLY vertex_idx_node_id ON dickens."_ag_label_vertex" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
create INDEX CONCURRENTLY entity_idx_node_id ON dickens."Entity" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
CREATE INDEX CONCURRENTLY entity_node_id_gin_idx ON dickens."Entity" using gin(properties);
ALTER TABLE dickens."DIRECTED" CLUSTER ON directed_sid_idx;
-- 如有必要可以删除
drop INDEX entity_p_idx;
drop INDEX vertex_p_idx;
drop INDEX directed_p_idx;
drop INDEX directed_eid_idx;
drop INDEX directed_sid_idx;
drop INDEX directed_seid_idx;
drop INDEX edge_p_idx;
drop INDEX edge_sid_idx;
drop INDEX edge_eid_idx;
drop INDEX edge_seid_idx;
drop INDEX vertex_idx_node_id;
drop INDEX entity_idx_node_id;
drop INDEX entity_node_id_gin_idx;
```
* Apache AGE的已知问题发布版本存在以下问题
> 您可能会发现节点/边的属性是空的。
> 这是发布版本的已知问题https://github.com/apache/age/pull/1721
>
> 您可以从源代码编译AGE来修复它。
>
* Apache AGE的性能不如Neo4j。最求高性能的图数据库请使用Noe4j。
</details>
@ -1229,7 +1184,6 @@ LightRAG 现已与 [RAG-Anything](https://github.com/HKUDS/RAG-Anything) 实现
),
embedding_func=EmbeddingFunc(
embedding_dim=3072,
max_token_size=8192,
func=lambda texts: openai_embed(
texts,
model="text-embedding-3-large",

View File

@ -218,12 +218,12 @@ async def main():
try:
# Initialize RAG instance
rag = await initialize_rag()
rag.insert("Your text")
await rag.ainsert("Your text")
# Perform hybrid search
mode = "hybrid"
print(
await rag.query(
await rag.aquery(
"What are the top themes in this story?",
param=QueryParam(mode=mode)
)
@ -272,14 +272,13 @@ A full list of LightRAG init parameters:
| **embedding_func_max_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
| **llm_model_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
| **llm_model_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
| **llm_model_max_token_size** | `int` | Maximum tokens send to LLM to generate entity relation summaries | `32000`default value changed by env var MAX_TOKENS) |
| **summary_max_tokens** | `int` | Maximum tokens send to LLM to generate entity relation summaries | `32000`default value changed by env var MAX_TOKENS) |
| **llm_model_max_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `4`default value changed by env var MAX_ASYNC) |
| **llm_model_kwargs** | `dict` | Additional parameters for LLM generation | |
| **vector_db_storage_cls_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval | cosine_better_than_threshold: 0.2default value changed by env var COSINE_THRESHOLD) |
| **enable_llm_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
| **enable_llm_cache_for_entity_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
| **addon_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"]}`: sets example limit, entiy/relation extraction output language | `example_number: all examples, language: English` |
| **convert_response_to_json_func** | `callable` | Not used | `convert_response_to_json` |
| **embedding_cache_config** | `dict` | Configuration for question-answer caching. Contains three parameters: `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers. `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM. `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
</details>
@ -327,7 +326,7 @@ class QueryParam:
max_relation_tokens: int = int(os.getenv("MAX_RELATION_TOKENS", "10000"))
"""Maximum number of tokens allocated for relationship context in unified token control system."""
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "32000"))
max_total_tokens: int = int(os.getenv("MAX_TOTAL_TOKENS", "30000"))
"""Maximum total tokens budget for the entire query context (entities + relations + chunks + system prompt)."""
conversation_history: list[dict[str, str]] = field(default_factory=list)
@ -335,7 +334,8 @@ class QueryParam:
Format: [{"role": "user/assistant", "content": "message"}].
"""
history_turns: int = 3
# Deprated: history message have negtive effect on query performance
history_turns: int = 0
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
ids: list[str] | None = None
@ -397,7 +397,6 @@ async def initialize_rag():
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=4096,
max_token_size=8192,
func=embedding_func
)
)
@ -426,7 +425,6 @@ rag = LightRAG(
# Use Hugging Face embedding function
embedding_func=EmbeddingFunc(
embedding_dim=384,
max_token_size=5000,
func=lambda texts: hf_embed(
texts,
tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2"),
@ -455,7 +453,6 @@ rag = LightRAG(
# Use Ollama embedding function
embedding_func=EmbeddingFunc(
embedding_dim=768,
max_token_size=8192,
func=lambda texts: ollama_embed(
texts,
embed_model="nomic-embed-text"
@ -507,7 +504,6 @@ rag = LightRAG(
# Use Ollama embedding function
embedding_func=EmbeddingFunc(
embedding_dim=768,
max_token_size=8192,
func=lambda texts: ollama_embed(
texts,
embed_model="nomic-embed-text"
@ -550,7 +546,6 @@ async def initialize_rag():
llm_model_func=llama_index_complete_if_cache, # LlamaIndex-compatible completion function
embedding_func=EmbeddingFunc( # LlamaIndex-compatible embedding function
embedding_dim=1536,
max_token_size=8192,
func=lambda texts: llama_index_embed(texts, embed_model=embed_model)
),
)
@ -619,7 +614,6 @@ conversation_history = [
query_param = QueryParam(
mode="mix", # or any other mode: "local", "global", "hybrid"
conversation_history=conversation_history, # Add the conversation history
history_turns=3 # Number of recent conversation turns to consider
)
# Make a query that takes into account the conversation history
@ -797,58 +791,18 @@ see test_neo4j.py for a working example.
<details>
<summary> <b>Using PostgreSQL for Storage</b> </summary>
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE). PostgreSQL version 16.6 or higher is supported.
* PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
* If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
* How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
* Create index for AGE example: (Change below `dickens` to your graph name if necessary)
```sql
load 'age';
SET search_path = ag_catalog, "$user", public;
CREATE INDEX CONCURRENTLY entity_p_idx ON dickens."Entity" (id);
CREATE INDEX CONCURRENTLY vertex_p_idx ON dickens."_ag_label_vertex" (id);
CREATE INDEX CONCURRENTLY directed_p_idx ON dickens."DIRECTED" (id);
CREATE INDEX CONCURRENTLY directed_eid_idx ON dickens."DIRECTED" (end_id);
CREATE INDEX CONCURRENTLY directed_sid_idx ON dickens."DIRECTED" (start_id);
CREATE INDEX CONCURRENTLY directed_seid_idx ON dickens."DIRECTED" (start_id,end_id);
CREATE INDEX CONCURRENTLY edge_p_idx ON dickens."_ag_label_edge" (id);
CREATE INDEX CONCURRENTLY edge_sid_idx ON dickens."_ag_label_edge" (start_id);
CREATE INDEX CONCURRENTLY edge_eid_idx ON dickens."_ag_label_edge" (end_id);
CREATE INDEX CONCURRENTLY edge_seid_idx ON dickens."_ag_label_edge" (start_id,end_id);
create INDEX CONCURRENTLY vertex_idx_node_id ON dickens."_ag_label_vertex" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
create INDEX CONCURRENTLY entity_idx_node_id ON dickens."Entity" (ag_catalog.agtype_access_operator(properties, '"node_id"'::agtype));
CREATE INDEX CONCURRENTLY entity_node_id_gin_idx ON dickens."Entity" using gin(properties);
ALTER TABLE dickens."DIRECTED" CLUSTER ON directed_sid_idx;
-- drop if necessary
drop INDEX entity_p_idx;
drop INDEX vertex_p_idx;
drop INDEX directed_p_idx;
drop INDEX directed_eid_idx;
drop INDEX directed_sid_idx;
drop INDEX directed_seid_idx;
drop INDEX edge_p_idx;
drop INDEX edge_sid_idx;
drop INDEX edge_eid_idx;
drop INDEX edge_seid_idx;
drop INDEX vertex_idx_node_id;
drop INDEX entity_idx_node_id;
drop INDEX entity_node_id_gin_idx;
```
* Known issue of the Apache AGE: The released versions got below issue:
> You might find that the properties of the nodes/edges are empty.
> It is a known issue of the release version: https://github.com/apache/age/pull/1721
>
> You can Compile the AGE from source code and fix it.
>
* For high-performance graph database requirements, Neo4j is recommended as Apache AGE's performance is not as competitive.
</details>
<details>
<summary> <b>Using Faiss for Storage</b> </summary>
You must manually install faiss-cpu or faiss-gpu before using FAISS vector db.
Manually install `faiss-cpu` or `faiss-gpu` before using FAISS vector db.
Before using Faiss vector database, you must manually install `faiss-cpu` or `faiss-gpu`.
- Install the required dependencies:
@ -872,7 +826,6 @@ rag = LightRAG(
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=384,
max_token_size=8192,
func=embedding_func,
),
vector_storage="FaissVectorDBStorage",
@ -1278,7 +1231,6 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
),
embedding_func=EmbeddingFunc(
embedding_dim=3072,
max_token_size=8192,
func=lambda texts: openai_embed(
texts,
model="text-embedding-3-large",
@ -1287,10 +1239,8 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
),
)
)
# Initialize storage (this will load existing data if available)
await lightrag_instance.initialize_storages()
# Now initialize RAGAnything with the existing LightRAG instance
rag = RAGAnything(
lightrag=lightrag_instance, # Pass the existing LightRAG instance
@ -1319,14 +1269,12 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
)
# Note: working_dir, llm_model_func, embedding_func, etc. are inherited from lightrag_instance
)
# Query the existing knowledge base
result = await rag.query_with_multimodal(
"What data has been processed in this LightRAG instance?",
mode="hybrid"
)
print("Query result:", result)
# Add new multimodal documents to the existing LightRAG instance
await rag.process_document_complete(
file_path="path/to/new/multimodal_document.pdf",

View File

@ -84,7 +84,6 @@ LightRAG can be configured using environment variables in the `.env` file:
- `MAX_ASYNC`: Maximum async operations
- `MAX_TOKENS`: Maximum token size
- `EMBEDDING_DIM`: Embedding dimensions
- `MAX_EMBED_TOKENS`: Maximum embedding token size
#### Security
- `LIGHTRAG_API_KEY`: API key for authentication

View File

@ -6,7 +6,7 @@ LightRAG employs a multi-layered concurrent control strategy when processing mul
**Control Parameter**: `max_parallel_insert`
This parameter controls the number of documents processed simultaneously. The purpose is to prevent excessive parallelism from overwhelming system resources, which could lead to extended processing times for individual files. Document-level concurrency is governed by the `max_parallel_insert` attribute within LightRAG, which defaults to 2 and is configurable via the `MAX_PARALLEL_INSERT` environment variable.
This parameter controls the number of documents processed simultaneously. The purpose is to prevent excessive parallelism from overwhelming system resources, which could lead to extended processing times for individual files. Document-level concurrency is governed by the `max_parallel_insert` attribute within LightRAG, which defaults to 2 and is configurable via the `MAX_PARALLEL_INSERT` environment variable. `max_parallel_insert` is recommended to be set between 2 and 10, typically `llm_model_max_async/3`. Setting this value too high can increase the likelihood of naming conflicts among entities and relationships across different documents during the merge phase, thereby reducing its overall efficiency.
### 2. Chunk-Level Concurrent Control

View File

@ -7,7 +7,6 @@ HOST=0.0.0.0
PORT=9621
WEBUI_TITLE='My Graph KB'
WEBUI_DESCRIPTION="Simple and Fast Graph Based RAG System"
OLLAMA_EMULATING_MODEL_TAG=latest
# WORKERS=2
# CORS_ORIGINS=http://localhost:3000,http://localhost:8080
@ -21,6 +20,10 @@ OLLAMA_EMULATING_MODEL_TAG=latest
# INPUT_DIR=<absolute_path_for_doc_input_dir>
# WORKING_DIR=<absolute_path_for_working_dir>
### Ollama Emulating Model and Tag
# OLLAMA_EMULATING_MODEL_NAME=lightrag
OLLAMA_EMULATING_MODEL_TAG=latest
### Max nodes return from grap retrieval in webui
# MAX_GRAPH_NODES=1000
@ -45,30 +48,36 @@ OLLAMA_EMULATING_MODEL_TAG=latest
# LIGHTRAG_API_KEY=your-secure-api-key-here
# WHITELIST_PATHS=/health,/api/*
########################
######################################################################################
### Query Configuration
########################
# LLM responde cache for query (Not valid for streaming response
###
### How to control the context lenght sent to LLM:
### MAX_ENTITY_TOKENS + MAX_RELATION_TOKENS < MAX_TOTAL_TOKENS
### Chunk_Tokens = MAX_TOTAL_TOKENS - Actual_Entity_Tokens - Actual_Reation_Tokens
######################################################################################
# LLM responde cache for query (Not valid for streaming response)
ENABLE_LLM_CACHE=true
# HISTORY_TURNS=0
# COSINE_THRESHOLD=0.2
### Number of entities or relations retrieved from KG
# TOP_K=40
### Maxmium number or chunks plan to send to LLM
### Maxmium number or chunks for naive vactor search
# CHUNK_TOP_K=10
### control the actual enties send to LLM
# MAX_ENTITY_TOKENS=10000
### control the actual relations send to LLM
# MAX_RELATION_TOKENS=10000
### control the maximum tokens send to LLM (include entities, raltions and chunks)
# MAX_TOTAL_TOKENS=32000
### maxumium related chunks grab from single entity or relations
# RELATED_CHUNK_NUMBER=10
# MAX_TOTAL_TOKENS=30000
### maximum number of related chunks per source entity or relation (higher values increase re-ranking time)
# RELATED_CHUNK_NUMBER=5
### Reranker configuration (Set ENABLE_RERANK to true in reranking model is configed)
ENABLE_RERANK=False
# RERANK_MODEL=BAAI/bge-reranker-v2-m3
# RERANK_BINDING_HOST=https://api.your-rerank-provider.com/v1/rerank
# ENABLE_RERANK=True
### Minimum rerank score for document chunk exclusion (set to 0.0 to keep all chunks, 0.6 or above if LLM is not strong enought)
# MIN_RERANK_SCORE=0.0
### Rerank model configuration (required when ENABLE_RERANK=True)
# RERANK_MODEL=jina-reranker-v2-base-multilingual
# RERANK_BINDING_HOST=https://api.jina.ai/v1/rerank
# RERANK_BINDING_API_KEY=your_rerank_api_key_here
########################################
@ -77,14 +86,13 @@ ENABLE_RERANK=False
### Language: English, Chinese, French, German ...
SUMMARY_LANGUAGE=English
ENABLE_LLM_CACHE_FOR_EXTRACT=true
### MAX_TOKENS: max tokens send to LLM for entity relation summaries (less than context size of the model)
MAX_TOKENS=32000
### Chunk size for document splitting, 500~1500 is recommended
# CHUNK_SIZE=1200
# CHUNK_OVERLAP_SIZE=100
### Entity and relation summarization configuration
### Number of duplicated entities/edges to trigger LLM re-summary on merge ( at least 3 is recommented)
### Number of duplicated entities/edges to trigger LLM re-summary on merge (at least 3 is recommented) and max tokens send to LLM
# FORCE_LLM_SUMMARY_ON_MERGE=4
# MAX_TOKENS=10000
### Maximum number of entity extraction attempts for ambiguous content
# MAX_GLEANING=1
@ -93,7 +101,7 @@ MAX_TOKENS=32000
###############################
### Max concurrency requests of LLM (for both query and document processing)
MAX_ASYNC=4
### Number of parallel processing documents(between 2~10, MAX_ASYNC/4 is recommended)
### Number of parallel processing documents(between 2~10, MAX_ASYNC/3 is recommended)
MAX_PARALLEL_INSERT=2
### Max concurrency requests for Embedding
# EMBEDDING_FUNC_MAX_ASYNC=8
@ -103,18 +111,21 @@ MAX_PARALLEL_INSERT=2
#######################
### LLM Configuration
#######################
### Time out in seconds for LLM, None for infinite timeout
TIMEOUT=240
### Some models like o1-mini require temperature to be set to 1
TEMPERATURE=0
### Some models like o1-mini require temperature to be set to 1, some LLM can fall into output loops with low temperature
# TEMPERATURE=1.0
### LLM Binding type: openai, ollama, lollms, azure_openai
LLM_BINDING=openai
LLM_MODEL=gpt-4o
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
### Set as num_ctx option for Ollama LLM
# OLLAMA_NUM_CTX=32768
### Most Commont Parameters for Ollama Server
### see also env.ollama-binding-options.example for fine tuning ollama
### OLLAMA_LLM_NUM_CTX must be larger than MAX_TOTAL_TOKENS + 2000
# OLLAMA_LLM_NUM_CTX=32768
### Time out in seconds, None for infinite timeout
TIMEOUT=240
### Optional for Azure
# AZURE_OPENAI_API_VERSION=2024-08-01-preview
@ -123,15 +134,22 @@ LLM_BINDING_API_KEY=your_api_key
####################################################################################
### Embedding Configuration (Should not be changed after the first file processed)
####################################################################################
### Embedding Binding type: openai, ollama, lollms, azure_openai
### Embedding Binding type: openai, ollama, lollms, azure_openai, jina
### see also env.ollama-binding-options.example for fine tuning ollama
EMBEDDING_BINDING=ollama
EMBEDDING_MODEL=bge-m3:latest
EMBEDDING_DIM=1024
EMBEDDING_BINDING_API_KEY=your_api_key
# If the embedding service is deployed within the same Docker stack, use host.docker.internal instead of localhost
EMBEDDING_BINDING_HOST=http://localhost:11434
### Maximum tokens sent to Embedding for each chunk (no longer in use?)
# MAX_EMBED_TOKENS=8192
### OpenAI compatible
# EMBEDDING_BINDING=openai
# EMBEDDING_MODEL=text-embedding-3-large
# EMBEDDING_DIM=3072
# EMBEDDING_BINDING_HOST=https://api.openai.com
# EMBEDDING_BINDING_API_KEY=your_api_key
### Optional for Azure
# AZURE_EMBEDDING_DEPLOYMENT=text-embedding-3-large
@ -139,6 +157,20 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
# AZURE_EMBEDDING_ENDPOINT=your_endpoint
# AZURE_EMBEDDING_API_KEY=your_api_key
### Jina AI Embedding
EMBEDDING_BINDING=jina
EMBEDDING_BINDING_HOST=https://api.jina.ai/v1/embeddings
EMBEDDING_MODEL=jina-embeddings-v4
EMBEDDING_DIM=2048
EMBEDDING_BINDING_API_KEY=your_api_key
####################################################################
### WORKSPACE setting workspace name for all storage types
### in the purpose of isolating data from LightRAG instances.
### Valid workspace name constraints: a-z, A-Z, 0-9, and _
####################################################################
# WORKSPACE=space1
############################
### Data storage selection
############################
@ -173,13 +205,6 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
# LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage
# LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage
####################################################################
### WORKSPACE setting workspace name for all storage types
### in the purpose of isolating data from LightRAG instances.
### Valid workspace name constraints: a-z, A-Z, 0-9, and _
####################################################################
# WORKSPACE=space1
### PostgreSQL Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432

View File

@ -0,0 +1,195 @@
################################################################################
# Autogenerated .env entries list for LightRAG binding options
#
# To generate run:
# $ python -m lightrag.llm.binding_options
################################################################################
# ollama_embedding -- Context window size (number of tokens)
# OLLAMA_EMBEDDING_NUM_CTX=4096
# ollama_embedding -- Maximum number of tokens to predict
# OLLAMA_EMBEDDING_NUM_PREDICT=128
# ollama_embedding -- Number of tokens to keep from the initial prompt
# OLLAMA_EMBEDDING_NUM_KEEP=0
# ollama_embedding -- Random seed for generation (-1 for random)
# OLLAMA_EMBEDDING_SEED=-1
# ollama_embedding -- Controls randomness (0.0-2.0, higher = more creative)
# OLLAMA_EMBEDDING_TEMPERATURE=0.8
# ollama_embedding -- Top-k sampling parameter (0 = disabled)
# OLLAMA_EMBEDDING_TOP_K=40
# ollama_embedding -- Top-p (nucleus) sampling parameter (0.0-1.0)
# OLLAMA_EMBEDDING_TOP_P=0.9
# ollama_embedding -- Tail free sampling parameter (1.0 = disabled)
# OLLAMA_EMBEDDING_TFS_Z=1.0
# ollama_embedding -- Typical probability mass (1.0 = disabled)
# OLLAMA_EMBEDDING_TYPICAL_P=1.0
# ollama_embedding -- Minimum probability threshold (0.0 = disabled)
# OLLAMA_EMBEDDING_MIN_P=0.0
# ollama_embedding -- Number of tokens to consider for repetition penalty
# OLLAMA_EMBEDDING_REPEAT_LAST_N=64
# ollama_embedding -- Penalty for repetition (1.0 = no penalty)
# OLLAMA_EMBEDDING_REPEAT_PENALTY=1.1
# ollama_embedding -- Penalty for token presence (-2.0 to 2.0)
# OLLAMA_EMBEDDING_PRESENCE_PENALTY=0.0
# ollama_embedding -- Penalty for token frequency (-2.0 to 2.0)
# OLLAMA_EMBEDDING_FREQUENCY_PENALTY=0.0
# ollama_embedding -- Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
# OLLAMA_EMBEDDING_MIROSTAT=0
# ollama_embedding -- Mirostat target entropy
# OLLAMA_EMBEDDING_MIROSTAT_TAU=5.0
# ollama_embedding -- Mirostat learning rate
# OLLAMA_EMBEDDING_MIROSTAT_ETA=0.1
# ollama_embedding -- Enable NUMA optimization
# OLLAMA_EMBEDDING_NUMA=False
# ollama_embedding -- Batch size for processing
# OLLAMA_EMBEDDING_NUM_BATCH=512
# ollama_embedding -- Number of GPUs to use (-1 for auto)
# OLLAMA_EMBEDDING_NUM_GPU=-1
# ollama_embedding -- Main GPU index
# OLLAMA_EMBEDDING_MAIN_GPU=0
# ollama_embedding -- Optimize for low VRAM
# OLLAMA_EMBEDDING_LOW_VRAM=False
# ollama_embedding -- Number of CPU threads (0 for auto)
# OLLAMA_EMBEDDING_NUM_THREAD=0
# ollama_embedding -- Use half-precision for key/value cache
# OLLAMA_EMBEDDING_F16_KV=True
# ollama_embedding -- Return logits for all tokens
# OLLAMA_EMBEDDING_LOGITS_ALL=False
# ollama_embedding -- Only load vocabulary
# OLLAMA_EMBEDDING_VOCAB_ONLY=False
# ollama_embedding -- Use memory mapping for model files
# OLLAMA_EMBEDDING_USE_MMAP=True
# ollama_embedding -- Lock model in memory
# OLLAMA_EMBEDDING_USE_MLOCK=False
# ollama_embedding -- Only use for embeddings
# OLLAMA_EMBEDDING_EMBEDDING_ONLY=False
# ollama_embedding -- Penalize newline tokens
# OLLAMA_EMBEDDING_PENALIZE_NEWLINE=True
# ollama_embedding -- Stop sequences (comma-separated string)
# OLLAMA_EMBEDDING_STOP=
# ollama_llm -- Context window size (number of tokens)
# OLLAMA_LLM_NUM_CTX=4096
# ollama_llm -- Maximum number of tokens to predict
# OLLAMA_LLM_NUM_PREDICT=128
# ollama_llm -- Number of tokens to keep from the initial prompt
# OLLAMA_LLM_NUM_KEEP=0
# ollama_llm -- Random seed for generation (-1 for random)
# OLLAMA_LLM_SEED=-1
# ollama_llm -- Controls randomness (0.0-2.0, higher = more creative)
# OLLAMA_LLM_TEMPERATURE=0.8
# ollama_llm -- Top-k sampling parameter (0 = disabled)
# OLLAMA_LLM_TOP_K=40
# ollama_llm -- Top-p (nucleus) sampling parameter (0.0-1.0)
# OLLAMA_LLM_TOP_P=0.9
# ollama_llm -- Tail free sampling parameter (1.0 = disabled)
# OLLAMA_LLM_TFS_Z=1.0
# ollama_llm -- Typical probability mass (1.0 = disabled)
# OLLAMA_LLM_TYPICAL_P=1.0
# ollama_llm -- Minimum probability threshold (0.0 = disabled)
# OLLAMA_LLM_MIN_P=0.0
# ollama_llm -- Number of tokens to consider for repetition penalty
# OLLAMA_LLM_REPEAT_LAST_N=64
# ollama_llm -- Penalty for repetition (1.0 = no penalty)
# OLLAMA_LLM_REPEAT_PENALTY=1.1
# ollama_llm -- Penalty for token presence (-2.0 to 2.0)
# OLLAMA_LLM_PRESENCE_PENALTY=0.0
# ollama_llm -- Penalty for token frequency (-2.0 to 2.0)
# OLLAMA_LLM_FREQUENCY_PENALTY=0.0
# ollama_llm -- Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
# OLLAMA_LLM_MIROSTAT=0
# ollama_llm -- Mirostat target entropy
# OLLAMA_LLM_MIROSTAT_TAU=5.0
# ollama_llm -- Mirostat learning rate
# OLLAMA_LLM_MIROSTAT_ETA=0.1
# ollama_llm -- Enable NUMA optimization
# OLLAMA_LLM_NUMA=False
# ollama_llm -- Batch size for processing
# OLLAMA_LLM_NUM_BATCH=512
# ollama_llm -- Number of GPUs to use (-1 for auto)
# OLLAMA_LLM_NUM_GPU=-1
# ollama_llm -- Main GPU index
# OLLAMA_LLM_MAIN_GPU=0
# ollama_llm -- Optimize for low VRAM
# OLLAMA_LLM_LOW_VRAM=False
# ollama_llm -- Number of CPU threads (0 for auto)
# OLLAMA_LLM_NUM_THREAD=0
# ollama_llm -- Use half-precision for key/value cache
# OLLAMA_LLM_F16_KV=True
# ollama_llm -- Return logits for all tokens
# OLLAMA_LLM_LOGITS_ALL=False
# ollama_llm -- Only load vocabulary
# OLLAMA_LLM_VOCAB_ONLY=False
# ollama_llm -- Use memory mapping for model files
# OLLAMA_LLM_USE_MMAP=True
# ollama_llm -- Lock model in memory
# OLLAMA_LLM_USE_MLOCK=False
# ollama_llm -- Only use for embeddings
# OLLAMA_LLM_EMBEDDING_ONLY=False
# ollama_llm -- Penalize newline tokens
# OLLAMA_LLM_PENALIZE_NEWLINE=True
# ollama_llm -- Stop sequences (comma-separated string)
# OLLAMA_LLM_STOP=
#
# End of .env entries for LightRAG binding options
################################################################################

View File

@ -87,7 +87,7 @@ async def initialize_rag():
working_dir=WORKING_DIR,
llm_model_func=ollama_model_complete,
llm_model_name=os.getenv("LLM_MODEL", "qwen2.5-coder:7b"),
llm_model_max_token_size=8192,
summary_max_tokens=8192,
llm_model_kwargs={
"host": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"),
"options": {"num_ctx": 8192},

View File

@ -211,7 +211,7 @@ async def initialize_rag():
max_parallel_insert=2,
llm_model_func=cloudflare_worker.query,
llm_model_name=os.getenv("LLM_MODEL", LLM_MODEL),
llm_model_max_token_size=4080,
summary_max_tokens=4080,
embedding_func=EmbeddingFunc(
embedding_dim=int(os.getenv("EMBEDDING_DIM", "1024")),
max_token_size=int(os.getenv("MAX_EMBED_TOKENS", "2048")),

View File

@ -56,7 +56,7 @@ async def initialize_rag():
rag = LightRAG(
working_dir=WORKING_DIR,
llm_model_func=llm_model_func,
llm_model_max_token_size=32768,
summary_max_tokens=10000,
embedding_func=embedding_func,
chunk_token_size=512,
chunk_overlap_token_size=256,

View File

@ -1,5 +1,5 @@
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
__version__ = "1.4.4"
__version__ = "1.4.6"
__author__ = "Zirui Guo"
__url__ = "https://github.com/HKUDS/LightRAG"

View File

@ -54,8 +54,6 @@ LLM_BINDING=openai
LLM_MODEL=gpt-4o
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
### 发送给 LLM 进行实体关系摘要的最大 token 数(小于模型上下文大小)
MAX_TOKENS=32000
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
@ -71,10 +69,8 @@ LLM_BINDING=ollama
LLM_MODEL=mistral-nemo:latest
LLM_BINDING_HOST=http://localhost:11434
# LLM_BINDING_API_KEY=your_api_key
### 发送给 LLM 进行实体关系摘要的最大 token 数(小于模型上下文大小)
MAX_TOKENS=7500
### Ollama 服务器上下文 token 数(基于您的 Ollama 服务器容量)
OLLAMA_NUM_CTX=8192
### Ollama 服务器上下文 token 数(必须大于 MAX_TOTAL_TOKENS+2000
OLLAMA_LLM_NUM_CTX=8192
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
@ -419,6 +415,8 @@ PGDocStatusStorage Postgres
MongoDocStatusStorage MongoDB
```
每一种存储类型的链接配置范例可以在 `env.example` 文件中找到。链接字符串中的数据库实例是需要你预先在数据库服务器上创建好的LightRAG 仅负责在数据库实例中创建数据表,不负责创建数据库实例。如果使用 Redis 作为存储,记得给 Redis 配置自动持久化数据规则,否则 Redis 服务重启后数据会丢失。如果使用PostgreSQL数据库推荐使用16.6版本或以上。
### 如何选择存储实现
您可以通过环境变量选择存储实现。在首次启动 API 服务器之前,您可以将以下环境变量设置为特定的存储实现名称:
@ -470,9 +468,7 @@ MAX_PARALLEL_INSERT=2
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
TIMEOUT=200
TEMPERATURE=0.0
MAX_ASYNC=4
MAX_TOKENS=32768
LLM_BINDING=openai
LLM_MODEL=gpt-4o-mini
@ -568,12 +564,12 @@ pip install lightrag-hku
LightRAG 中的文档处理流程有些复杂,分为两个主要阶段:提取阶段(实体和关系提取)和合并阶段(实体和关系合并)。有两个关键参数控制流程并发性:并行处理的最大文件数(`MAX_PARALLEL_INSERT`)和最大并发 LLM 请求数(`MAX_ASYNC`)。工作流程描述如下:
1. `MAX_PARALLEL_INSERT` 控制提取阶段并行处理的文件数量
2. `MAX_ASYNC` 限制系统中并发 LLM 请求的总数包括查询、提取和合并的请求。LLM 请求具有不同的优先级:查询操作优先级最高,其次是合并,然后是提取
1. `MAX_ASYNC` 限制系统中并发 LLM 请求的总数包括查询、提取和合并的请求。LLM 请求具有不同的优先级:查询操作优先级最高,其次是合并,然后是提取
2. `MAX_PARALLEL_INSERT` 控制提取阶段并行处理的文件数量。`MAX_PARALLEL_INSERT`建议设置为210之间通常设置为 `MAX_ASYNC/3`,设置太大会导致合并阶段不同文档之间实体和关系重名的机会增大,降低合并阶段的效率
3. 在单个文件中,来自不同文本块的实体和关系提取是并发处理的,并发度由 `MAX_ASYNC` 设置。只有在处理完 `MAX_ASYNC` 个文本块后,系统才会继续处理同一文件中的下一批文本块。
4. 合并阶段仅在文件中所有文本块完成实体和关系提取后开始。当一个文件进入合并阶段时,流程允许下一个文件开始提取
5. 由于提取阶段通常比合并阶段快,因此实际并发处理的文件数可能会超过 `MAX_PARALLEL_INSERT`,因为此参数仅控制提取阶段的并行度
6. 为防止竞争条件,合并阶段不支持多个文件的并发处理;一次只能合并一个文件,其他文件必须在队列中等待
4. 当一个文件完成实体和关系提后,将进入实体和关系合并阶段。这一阶段也会并发处理多个实体和关系,其并发度同样是由 `MAX_ASYNC` 控制
5. 合并阶段的 LLM 请求的优先级别高于提取阶段,目的是让进入合并阶段的文件尽快完成处理,并让处理结果尽快更新到向量数据库中
6. 为防止竞争条件,合并阶段会避免并发处理同一个实体或关系,当多个文件中都涉及同一个实体或关系需要合并的时候他们会串行执行
7. 每个文件在流程中被视为一个原子处理单元。只有当其所有文本块都完成提取和合并后,文件才会被标记为成功处理。如果在处理过程中发生任何错误,整个文件将被标记为失败,并且必须重新处理。
8. 当由于错误而重新处理文件时,由于 LLM 缓存,先前处理的文本块可以快速跳过。尽管 LLM 缓存在合并阶段也会被利用,但合并顺序的不一致可能会限制其在此阶段的有效性。
9. 如果在提取过程中发生错误,系统不会保留任何中间结果。如果在合并过程中发生错误,已合并的实体和关系可能会被保留;当重新处理同一文件时,重新提取的实体和关系将与现有实体和关系合并,而不会影响查询结果。
@ -596,112 +592,20 @@ LightRAG 中的文档处理流程有些复杂,分为两个主要阶段:提
4. 使用查询端点查询系统
5. 如果在输入目录中放入新文件,触发文档扫描
### 查询端点
## 异步文档索引与进度跟踪
#### POST /query
使用不同搜索模式查询 RAG 系统。
LightRAG采用异步文档索引机制便于前端监控和查询文档处理进度。用户通过指定端点上传文件或插入文本时系统将返回唯一的跟踪ID以便实时监控处理进度。
```bash
curl -X POST "http://localhost:9621/query" \
-H "Content-Type: application/json" \
-d '{"query": "您的问题", "mode": "hybrid", ""}'
```
**支持生成跟踪ID的API端点**
* `/documents/upload`
* `/documents/text`
* `/documents/texts`
#### POST /query/stream
从 RAG 系统流式获取响应。
**文档处理状态查询端点:**
* `/track_status/{track_id}`
```bash
curl -X POST "http://localhost:9621/query/stream" \
-H "Content-Type: application/json" \
-d '{"query": "您的问题", "mode": "hybrid"}'
```
### 文档管理端点
#### POST /documents/text
直接将文本插入 RAG 系统。
```bash
curl -X POST "http://localhost:9621/documents/text" \
-H "Content-Type: application/json" \
-d '{"text": "您的文本内容", "description": "可选描述"}'
```
#### POST /documents/file
向 RAG 系统上传单个文件。
```bash
curl -X POST "http://localhost:9621/documents/file" \
-F "file=@/path/to/your/document.txt" \
-F "description=可选描述"
```
#### POST /documents/batch
一次上传多个文件。
```bash
curl -X POST "http://localhost:9621/documents/batch" \
-F "files=@/path/to/doc1.txt" \
-F "files=@/path/to/doc2.txt"
```
#### POST /documents/scan
触发输入目录中新文件的文档扫描。
```bash
curl -X POST "http://localhost:9621/documents/scan" --max-time 1800
```
> 根据所有新文件的预计索引时间调整 max-time。
#### DELETE /documents
从 RAG 系统中清除所有文档。
```bash
curl -X DELETE "http://localhost:9621/documents"
```
### Ollama 模拟端点
#### GET /api/version
获取 Ollama 版本信息。
```bash
curl http://localhost:9621/api/version
```
#### GET /api/tags
获取 Ollama 可用模型。
```bash
curl http://localhost:9621/api/tags
```
#### POST /api/chat
处理聊天补全请求。通过根据查询前缀选择查询模式将用户查询路由到 LightRAG。检测并将 OpenWebUI 会话相关请求(用于元数据生成任务)直接转发给底层 LLM。
```shell
curl -N -X POST http://localhost:9621/api/chat -H "Content-Type: application/json" -d \
'{"model":"lightrag:latest","messages":[{"role":"user","content":"猪八戒是谁"}],"stream":true}'
```
> 有关 Ollama API 的更多信息,请访问:[Ollama API 文档](https://github.com/ollama/ollama/blob/main/docs/api.md)
#### POST /api/generate
处理生成补全请求。为了兼容性目的,该请求不由 LightRAG 处理,而是由底层 LLM 模型处理。
### 实用工具端点
#### GET /health
检查服务器健康状况和配置。
```bash
curl "http://localhost:9621/health"
```
该端点提供全面的状态信息,包括:
* 文档处理状态(待处理/处理中/已处理/失败)
* 内容摘要和元数据
* 处理失败时的错误信息
* 创建和更新时间戳

View File

@ -54,8 +54,6 @@ LLM_BINDING=openai
LLM_MODEL=gpt-4o
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
### Max tokens sent to LLM (less than model context size)
MAX_TOKENS=32768
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
@ -71,10 +69,8 @@ LLM_BINDING=ollama
LLM_MODEL=mistral-nemo:latest
LLM_BINDING_HOST=http://localhost:11434
# LLM_BINDING_API_KEY=your_api_key
### Max tokens sent to LLM for entity relation description summarization (Less than LLM context length)
MAX_TOKENS=7500
### Ollama Server context length
OLLAMA_NUM_CTX=8192
### Ollama Server context length (Must be larger than MAX_TOTAL_TOKENS+2000)
OLLAMA_LLM_NUM_CTX=16384
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
@ -422,6 +418,8 @@ JsonDocStatusStorage JsonFile (default)
PGDocStatusStorage Postgres
MongoDocStatusStorage MongoDB
```
Example connection configurations for each storage type can be found in the `env.example` file. The database instance in the connection string needs to be created by you on the database server beforehand. LightRAG is only responsible for creating tables within the database instance, not for creating the database instance itself. If using Redis as storage, remember to configure automatic data persistence rules for Redis, otherwise data will be lost after the Redis service restarts. If using PostgreSQL, it is recommended to use version 16.6 or above.
### How to Select Storage Implementation
@ -459,6 +457,10 @@ You cannot change storage implementation selection after adding documents to Lig
| --embedding-binding | ollama | Embedding binding type (lollms, ollama, openai, azure_openai) |
| --auto-scan-at-startup| - | Scan input directory for new files and start indexing |
### Additional Ollama Binding Options
When using `--llm-binding ollama` or `--embedding-binding ollama`, additional Ollama-specific configuration options are available. To see all available Ollama binding options, add `--help` to the command line when starting the server. These additional options allow for fine-tuning of Ollama model parameters and connection settings.
### .env Examples
```bash
@ -474,9 +476,7 @@ MAX_PARALLEL_INSERT=2
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
TIMEOUT=200
TEMPERATURE=0.0
MAX_ASYNC=4
MAX_TOKENS=32768
LLM_BINDING=openai
LLM_MODEL=gpt-4o-mini
@ -484,6 +484,7 @@ LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your-api-key
### Embedding Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
# see also env.ollama-binding-options.example for fine tuning ollama
EMBEDDING_MODEL=bge-m3:latest
EMBEDDING_DIM=1024
EMBEDDING_BINDING=ollama
@ -504,12 +505,12 @@ EMBEDDING_BINDING_HOST=http://localhost:11434
The document processing pipeline in LightRAG is somewhat complex and is divided into two primary stages: the Extraction stage (entity and relationship extraction) and the Merging stage (entity and relationship merging). There are two key parameters that control pipeline concurrency: the maximum number of files processed in parallel (MAX_PARALLEL_INSERT) and the maximum number of concurrent LLM requests (MAX_ASYNC). The workflow is described as follows:
1. MAX_PARALLEL_INSERT controls the number of files processed in parallel during the extraction stage.
2. MAX_ASYNC limits the total number of concurrent LLM requests in the system, including those for querying, extraction, and merging. LLM requests have different priorities: query operations have the highest priority, followed by merging, and then extraction.
1. MAX_ASYNC limits the total number of concurrent LLM requests in the system, including those for querying, extraction, and merging. LLM requests have different priorities: query operations have the highest priority, followed by merging, and then extraction.
2. MAX_PARALLEL_INSERT controls the number of files processed in parallel during the extraction stage. For optimal performance, MAX_PARALLEL_INSERT is recommended to be set between 2 and 10, typically MAX_ASYNC/3. Setting this value too high can increase the likelihood of naming conflicts among entities and relationships across different documents during the merge phase, thereby reducing its overall efficiency.
3. Within a single file, entity and relationship extractions from different text blocks are processed concurrently, with the degree of concurrency set by MAX_ASYNC. Only after MAX_ASYNC text blocks are processed will the system proceed to the next batch within the same file.
4. The merging stage begins only after all text blocks in a file have completed entity and relationship extraction. When a file enters the merging stage, the pipeline allows the next file to begin extraction.
5. Since the extraction stage is generally faster than merging, the actual number of files being processed concurrently may exceed MAX_PARALLEL_INSERT, as this parameter only controls parallelism during the extraction stage.
6. To prevent race conditions, the merging stage does not support concurrent processing of multiple files; only one file can be merged at a time, while other files must wait in queue.
4. When a file completes entity and relationship extraction, it enters the entity and relationship merging stage. This stage also processes multiple entities and relationships concurrently, with the concurrency level also controlled by `MAX_ASYNC`.
5. LLM requests for the merging stage are prioritized over the extraction stage to ensure that files in the merging phase are processed quickly and their results are promptly updated in the vector database.
6. To prevent race conditions, the merging stage avoids concurrent processing of the same entity or relationship. When multiple files involve the same entity or relationship that needs to be merged, they are processed serially.
7. Each file is treated as an atomic processing unit in the pipeline. A file is marked as successfully processed only after all its text blocks have completed extraction and merging. If any error occurs during processing, the entire file is marked as failed and must be reprocessed.
8. When a file is reprocessed due to errors, previously processed text blocks can be quickly skipped thanks to LLM caching. Although LLM cache is also utilized during the merging stage, inconsistencies in merging order may limit its effectiveness in this stage.
9. If an error occurs during extraction, the system does not retain any intermediate results. If an error occurs during merging, already merged entities and relationships might be preserved; when the same file is reprocessed, re-extracted entities and relationships will be merged with the existing ones, without impacting the query results.
@ -532,111 +533,21 @@ You can test the API endpoints using the provided curl commands or through the S
4. Query the system using the query endpoints
5. Trigger document scan if new files are put into the inputs directory
### Query Endpoints:
## Asynchronous Document Indexing with Progress Tracking
#### POST /query
Query the RAG system with options for different search modes.
LightRAG implements asynchronous document indexing to enable frontend monitoring and querying of document processing progress. Upon uploading files or inserting text through designated endpoints, a unique Track ID is returned to facilitate real-time progress monitoring.
```bash
curl -X POST "http://localhost:9621/query" \
-H "Content-Type: application/json" \
-d '{"query": "Your question here", "mode": "hybrid"}'
```
**API Endpoints Supporting Track ID Generation:**
#### POST /query/stream
Stream responses from the RAG system.
* `/documents/upload`
* `/documents/text`
* `/documents/texts`
```bash
curl -X POST "http://localhost:9621/query/stream" \
-H "Content-Type: application/json" \
-d '{"query": "Your question here", "mode": "hybrid"}'
```
**Document Processing Status Query Endpoint:**
* `/track_status/{track_id}`
### Document Management Endpoints:
#### POST /documents/text
Insert text directly into the RAG system.
```bash
curl -X POST "http://localhost:9621/documents/text" \
-H "Content-Type: application/json" \
-d '{"text": "Your text content here", "description": "Optional description"}'
```
#### POST /documents/file
Upload a single file to the RAG system.
```bash
curl -X POST "http://localhost:9621/documents/file" \
-F "file=@/path/to/your/document.txt" \
-F "description=Optional description"
```
#### POST /documents/batch
Upload multiple files at once.
```bash
curl -X POST "http://localhost:9621/documents/batch" \
-F "files=@/path/to/doc1.txt" \
-F "files=@/path/to/doc2.txt"
```
#### POST /documents/scan
Trigger document scan for new files in the input directory.
```bash
curl -X POST "http://localhost:9621/documents/scan" --max-time 1800
```
> Adjust max-time according to the estimated indexing time for all new files.
#### DELETE /documents
Clear all documents from the RAG system.
```bash
curl -X DELETE "http://localhost:9621/documents"
```
### Ollama Emulation Endpoints:
#### GET /api/version
Get Ollama version information.
```bash
curl http://localhost:9621/api/version
```
#### GET /api/tags
Get available Ollama models.
```bash
curl http://localhost:9621/api/tags
```
#### POST /api/chat
Handle chat completion requests. Routes user queries through LightRAG by selecting query mode based on query prefix. Detects and forwards OpenWebUI session-related requests (for metadata generation task) directly to the underlying LLM.
```shell
curl -N -X POST http://localhost:9621/api/chat -H "Content-Type: application/json" -d \
'{"model":"lightrag:latest","messages":[{"role":"user","content":"猪八戒是谁"}],"stream":true}'
```
> For more information about Ollama API, please visit: [Ollama API documentation](https://github.com/ollama/ollama/blob/main/docs/api.md)
#### POST /api/generate
Handle generate completion requests. For compatibility purposes, the request is not processed by LightRAG, and will be handled by the underlying LLM model.
### Utility Endpoints:
#### GET /health
Check server health and configuration.
```bash
curl "http://localhost:9621/health"
```
This endpoint provides comprehensive status information including:
* Document processing status (pending/processing/processed/failed)
* Content summary and metadata
* Error messages if processing failed
* Timestamps for creation and updates

View File

@ -1 +1 @@
__api_version__ = "0187"
__api_version__ = "0196"

View File

@ -7,6 +7,9 @@ import argparse
import logging
from dotenv import load_dotenv
from lightrag.utils import get_env_value
from lightrag.llm.binding_options import OllamaEmbeddingOptions, OllamaLLMOptions
from lightrag.base import OllamaServerInfos
import sys
from lightrag.constants import (
DEFAULT_WOKERS,
@ -19,6 +22,16 @@ from lightrag.constants import (
DEFAULT_MAX_TOTAL_TOKENS,
DEFAULT_COSINE_THRESHOLD,
DEFAULT_RELATED_CHUNK_NUMBER,
DEFAULT_MIN_RERANK_SCORE,
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE,
DEFAULT_MAX_ASYNC,
DEFAULT_SUMMARY_MAX_TOKENS,
DEFAULT_SUMMARY_LANGUAGE,
DEFAULT_EMBEDDING_FUNC_MAX_ASYNC,
DEFAULT_EMBEDDING_BATCH_NUM,
DEFAULT_OLLAMA_MODEL_NAME,
DEFAULT_OLLAMA_MODEL_TAG,
DEFAULT_TEMPERATURE,
)
# use the .env that is inside the current folder
@ -27,16 +40,6 @@ from lightrag.constants import (
load_dotenv(dotenv_path=".env", override=False)
class OllamaServerInfos:
# Constants for emulated Ollama model information
LIGHTRAG_NAME = "lightrag"
LIGHTRAG_TAG = os.getenv("OLLAMA_EMULATING_MODEL_TAG", "latest")
LIGHTRAG_MODEL = f"{LIGHTRAG_NAME}:{LIGHTRAG_TAG}"
LIGHTRAG_SIZE = 7365960935 # it's a dummy value
LIGHTRAG_CREATED_AT = "2024-01-15T00:00:00Z"
LIGHTRAG_DIGEST = "sha256:lightrag"
ollama_server_infos = OllamaServerInfos()
@ -110,14 +113,14 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--max-async",
type=int,
default=get_env_value("MAX_ASYNC", 4, int),
help="Maximum async operations (default: from env or 4)",
default=get_env_value("MAX_ASYNC", DEFAULT_MAX_ASYNC, int),
help=f"Maximum async operations (default: from env or {DEFAULT_MAX_ASYNC})",
)
parser.add_argument(
"--max-tokens",
type=int,
default=get_env_value("MAX_TOKENS", 32000, int),
help="Maximum token size (default: from env or 32768)",
default=get_env_value("MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS, int),
help=f"Maximum token size (default: from env or {DEFAULT_SUMMARY_MAX_TOKENS})",
)
# Logging configuration
@ -159,14 +162,19 @@ def parse_args() -> argparse.Namespace:
help="Path to SSL private key file (required if --ssl is enabled)",
)
# Ollama model name
# Ollama model configuration
parser.add_argument(
"--simulated-model-name",
type=str,
default=get_env_value(
"SIMULATED_MODEL_NAME", ollama_server_infos.LIGHTRAG_MODEL
),
help="Number of conversation history turns to include (default: from env or 3)",
default=get_env_value("OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME),
help="Name for the simulated Ollama model (default: from env or lightrag)",
)
parser.add_argument(
"--simulated-model-tag",
type=str,
default=get_env_value("OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG),
help="Tag for the simulated Ollama model (default: from env or latest)",
)
# Namespace
@ -208,6 +216,29 @@ def parse_args() -> argparse.Namespace:
help="Embedding binding type (default: from env or ollama)",
)
# Conditionally add binding options defined in binding_options module
# This will add command line arguments for all binding options (e.g., --ollama-embedding-num_ctx)
# and corresponding environment variables (e.g., OLLAMA_EMBEDDING_NUM_CTX)
if "--llm-binding" in sys.argv:
try:
idx = sys.argv.index("--llm-binding")
if idx + 1 < len(sys.argv) and sys.argv[idx + 1] == "ollama":
OllamaLLMOptions.add_args(parser)
except IndexError:
pass
elif os.environ.get("LLM_BINDING") == "ollama":
OllamaLLMOptions.add_args(parser)
if "--embedding-binding" in sys.argv:
try:
idx = sys.argv.index("--embedding-binding")
if idx + 1 < len(sys.argv) and sys.argv[idx + 1] == "ollama":
OllamaEmbeddingOptions.add_args(parser)
except IndexError:
pass
elif os.environ.get("EMBEDDING_BINDING") == "ollama":
OllamaEmbeddingOptions.add_args(parser)
args = parser.parse_args()
# convert relative path to absolute path
@ -255,7 +286,6 @@ def parse_args() -> argparse.Namespace:
args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest")
args.embedding_model = get_env_value("EMBEDDING_MODEL", "bge-m3:latest")
args.embedding_dim = get_env_value("EMBEDDING_DIM", 1024, int)
args.max_embed_tokens = get_env_value("MAX_EMBED_TOKENS", 8192, int)
# Inject chunk configuration
args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int)
@ -268,14 +298,14 @@ def parse_args() -> argparse.Namespace:
args.enable_llm_cache = get_env_value("ENABLE_LLM_CACHE", True, bool)
# Inject LLM temperature configuration
args.temperature = get_env_value("TEMPERATURE", 0.5, float)
args.temperature = get_env_value("TEMPERATURE", DEFAULT_TEMPERATURE, float)
# Select Document loading tool (DOCLING, DEFAULT)
args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT")
# Add environment variables that were previously read directly
args.cors_origins = get_env_value("CORS_ORIGINS", "*")
args.summary_language = get_env_value("SUMMARY_LANGUAGE", "English")
args.summary_language = get_env_value("SUMMARY_LANGUAGE", DEFAULT_SUMMARY_LANGUAGE)
args.whitelist_paths = get_env_value("WHITELIST_PATHS", "/health,/api/*")
# For JWT Auth
@ -290,6 +320,11 @@ def parse_args() -> argparse.Namespace:
args.rerank_binding_host = get_env_value("RERANK_BINDING_HOST", None)
args.rerank_binding_api_key = get_env_value("RERANK_BINDING_API_KEY", None)
# Min rerank score configuration
args.min_rerank_score = get_env_value(
"MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float
)
# Query configuration
args.history_turns = get_env_value("HISTORY_TURNS", DEFAULT_HISTORY_TURNS, int)
args.top_k = get_env_value("TOP_K", DEFAULT_TOP_K, int)
@ -310,7 +345,19 @@ def parse_args() -> argparse.Namespace:
"RELATED_CHUNK_NUMBER", DEFAULT_RELATED_CHUNK_NUMBER, int
)
ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name
# Add missing environment variables for health endpoint
args.force_llm_summary_on_merge = get_env_value(
"FORCE_LLM_SUMMARY_ON_MERGE", DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE, int
)
args.embedding_func_max_async = get_env_value(
"EMBEDDING_FUNC_MAX_ASYNC", DEFAULT_EMBEDDING_FUNC_MAX_ASYNC, int
)
args.embedding_batch_num = get_env_value(
"EMBEDDING_BATCH_NUM", DEFAULT_EMBEDDING_BATCH_NUM, int
)
ollama_server_infos.LIGHTRAG_NAME = args.simulated_model_name
ollama_server_infos.LIGHTRAG_TAG = args.simulated_model_tag
return args

View File

@ -7,6 +7,8 @@ import asyncio
import os
import logging
import logging.config
import signal
import sys
import uvicorn
import pipmaster as pm
from fastapi.staticfiles import StaticFiles
@ -28,7 +30,6 @@ from .config import (
get_default_host,
)
from lightrag.utils import get_env_value
import sys
from lightrag import LightRAG, __version__ as core_version
from lightrag.api import __api_version__
from lightrag.types import GPTKeywordExtractionFormat
@ -53,6 +54,7 @@ from lightrag.kg.shared_storage import (
get_pipeline_status_lock,
initialize_pipeline_status,
cleanup_keyed_lock,
finalize_share_data,
)
from fastapi.security import OAuth2PasswordRequestForm
from lightrag.api.auth import auth_handler
@ -74,6 +76,24 @@ config.read("config.ini")
auth_configured = bool(auth_handler.accounts)
def setup_signal_handlers():
"""Setup signal handlers for graceful shutdown"""
def signal_handler(sig, frame):
print(f"\n\nReceived signal {sig}, shutting down gracefully...")
print(f"Process ID: {os.getpid()}")
# Release shared resources
finalize_share_data()
# Exit with success status
sys.exit(0)
# Register signal handlers
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
signal.signal(signal.SIGTERM, signal_handler) # kill command
def create_app(args):
# Setup logging
logger.setLevel(args.log_level)
@ -89,7 +109,13 @@ def create_app(args):
]:
raise Exception("llm binding not supported")
if args.embedding_binding not in ["lollms", "ollama", "openai", "azure_openai"]:
if args.embedding_binding not in [
"lollms",
"ollama",
"openai",
"azure_openai",
"jina",
]:
raise Exception("embedding binding not supported")
# Set default hosts if not provided
@ -153,6 +179,9 @@ def create_app(args):
# Clean up database connections
await rag.finalize_storages()
# Clean up shared data
finalize_share_data()
# Initialize FastAPI
app_kwargs = {
"title": "LightRAG Server API",
@ -203,6 +232,7 @@ def create_app(args):
from lightrag.llm.lollms import lollms_model_complete, lollms_embed
if args.llm_binding == "ollama" or args.embedding_binding == "ollama":
from lightrag.llm.ollama import ollama_model_complete, ollama_embed
from lightrag.llm.binding_options import OllamaLLMOptions
if args.llm_binding == "openai" or args.embedding_binding == "openai":
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
if args.llm_binding == "azure_openai" or args.embedding_binding == "azure_openai":
@ -213,6 +243,9 @@ def create_app(args):
if args.llm_binding_host == "openai-ollama" or args.embedding_binding == "ollama":
from lightrag.llm.openai import openai_complete_if_cache
from lightrag.llm.ollama import ollama_embed
from lightrag.llm.binding_options import OllamaEmbeddingOptions
if args.embedding_binding == "jina":
from lightrag.llm.jina import jina_embed
async def openai_alike_model_complete(
prompt,
@ -263,7 +296,6 @@ def create_app(args):
embedding_func = EmbeddingFunc(
embedding_dim=args.embedding_dim,
max_token_size=args.max_embed_tokens,
func=lambda texts: lollms_embed(
texts,
embed_model=args.embedding_model,
@ -276,6 +308,7 @@ def create_app(args):
embed_model=args.embedding_model,
host=args.embedding_binding_host,
api_key=args.embedding_binding_api_key,
options=OllamaEmbeddingOptions.options_dict(args),
)
if args.embedding_binding == "ollama"
else azure_openai_embed(
@ -284,6 +317,13 @@ def create_app(args):
api_key=args.embedding_binding_api_key,
)
if args.embedding_binding == "azure_openai"
else jina_embed(
texts,
dimensions=args.embedding_dim,
base_url=args.embedding_binding_host,
api_key=args.embedding_binding_api_key,
)
if args.embedding_binding == "jina"
else openai_embed(
texts,
model=args.embedding_model,
@ -320,6 +360,13 @@ def create_app(args):
"Rerank model not configured. Set RERANK_BINDING_API_KEY and RERANK_BINDING_HOST to enable reranking."
)
# Create ollama_server_infos from command line arguments
from lightrag.api.config import OllamaServerInfos
ollama_server_infos = OllamaServerInfos(
name=args.simulated_model_name, tag=args.simulated_model_tag
)
# Initialize RAG
if args.llm_binding in ["lollms", "ollama", "openai"]:
rag = LightRAG(
@ -332,13 +379,13 @@ def create_app(args):
else openai_alike_model_complete,
llm_model_name=args.llm_model,
llm_model_max_async=args.max_async,
llm_model_max_token_size=args.max_tokens,
summary_max_tokens=args.max_tokens,
chunk_token_size=int(args.chunk_size),
chunk_overlap_token_size=int(args.chunk_overlap_size),
llm_model_kwargs={
"host": args.llm_binding_host,
"timeout": args.timeout,
"options": {"num_ctx": args.ollama_num_ctx},
"options": OllamaLLMOptions.options_dict(args),
"api_key": args.llm_binding_api_key,
}
if args.llm_binding == "lollms" or args.llm_binding == "ollama"
@ -358,6 +405,7 @@ def create_app(args):
max_parallel_insert=args.max_parallel_insert,
max_graph_nodes=args.max_graph_nodes,
addon_params={"language": args.summary_language},
ollama_server_infos=ollama_server_infos,
)
else: # azure_openai
rag = LightRAG(
@ -371,7 +419,7 @@ def create_app(args):
},
llm_model_name=args.llm_model,
llm_model_max_async=args.max_async,
llm_model_max_token_size=args.max_tokens,
summary_max_tokens=args.max_tokens,
embedding_func=embedding_func,
kv_storage=args.kv_storage,
graph_storage=args.graph_storage,
@ -387,6 +435,7 @@ def create_app(args):
max_parallel_insert=args.max_parallel_insert,
max_graph_nodes=args.max_graph_nodes,
addon_params={"language": args.summary_language},
ollama_server_infos=ollama_server_infos,
)
# Add routes
@ -520,6 +569,16 @@ def create_app(args):
"rerank_binding_host": args.rerank_binding_host
if rerank_model_func is not None
else None,
# Environment variable status (requested configuration)
"summary_language": args.summary_language,
"force_llm_summary_on_merge": args.force_llm_summary_on_merge,
"max_parallel_insert": args.max_parallel_insert,
"cosine_threshold": args.cosine_threshold,
"min_rerank_score": args.min_rerank_score,
"related_chunk_number": args.related_chunk_number,
"max_async": args.max_async,
"embedding_func_max_async": args.embedding_func_max_async,
"embedding_batch_num": args.embedding_batch_num,
},
"auth_mode": auth_mode,
"pipeline_busy": pipeline_status.get("busy", False),
@ -701,6 +760,9 @@ def main():
update_uvicorn_mode_config()
display_splash_screen(global_args)
# Setup signal handlers for graceful shutdown
setup_signal_handlers()
# Create application instance directly instead of using factory function
app = create_app(global_args)

View File

@ -24,6 +24,7 @@ from pydantic import BaseModel, Field, field_validator
from lightrag import LightRAG
from lightrag.base import DeletionResult, DocProcessingStatus, DocStatus
from lightrag.utils import generate_track_id
from lightrag.api.utils_api import get_combined_auth_dependency
from ..config import global_args
@ -113,6 +114,7 @@ class ScanResponse(BaseModel):
Attributes:
status: Status of the scanning operation
message: Optional message with additional details
track_id: Tracking ID for monitoring scanning progress
"""
status: Literal["scanning_started"] = Field(
@ -121,12 +123,14 @@ class ScanResponse(BaseModel):
message: Optional[str] = Field(
default=None, description="Additional details about the scanning operation"
)
track_id: str = Field(description="Tracking ID for monitoring scanning progress")
class Config:
json_schema_extra = {
"example": {
"status": "scanning_started",
"message": "Scanning process has been initiated in the background",
"track_id": "scan_20250729_170612_abc123",
}
}
@ -210,18 +214,21 @@ class InsertResponse(BaseModel):
Attributes:
status: Status of the operation (success, duplicated, partial_success, failure)
message: Detailed message describing the operation result
track_id: Tracking ID for monitoring processing status
"""
status: Literal["success", "duplicated", "partial_success", "failure"] = Field(
description="Status of the operation"
)
message: str = Field(description="Message describing the operation result")
track_id: str = Field(description="Tracking ID for monitoring processing status")
class Config:
json_schema_extra = {
"example": {
"status": "success",
"message": "File 'document.pdf' uploaded successfully. Processing will continue in background.",
"track_id": "upload_20250729_170612_abc123",
}
}
@ -360,10 +367,13 @@ class DocStatusResponse(BaseModel):
status: DocStatus = Field(description="Current processing status")
created_at: str = Field(description="Creation timestamp (ISO format string)")
updated_at: str = Field(description="Last update timestamp (ISO format string)")
track_id: Optional[str] = Field(
default=None, description="Tracking ID for monitoring progress"
)
chunks_count: Optional[int] = Field(
default=None, description="Number of chunks the document was split into"
)
error: Optional[str] = Field(
error_msg: Optional[str] = Field(
default=None, description="Error message if processing failed"
)
metadata: Optional[dict[str, Any]] = Field(
@ -380,6 +390,7 @@ class DocStatusResponse(BaseModel):
"status": "PROCESSED",
"created_at": "2025-03-31T12:34:56",
"updated_at": "2025-03-31T12:35:30",
"track_id": "upload_20250729_170612_abc123",
"chunks_count": 12,
"error": None,
"metadata": {"author": "John Doe", "year": 2025},
@ -412,6 +423,10 @@ class DocsStatusesResponse(BaseModel):
"status": "PENDING",
"created_at": "2025-03-31T10:00:00",
"updated_at": "2025-03-31T10:00:00",
"track_id": "upload_20250331_100000_abc123",
"chunks_count": None,
"error": None,
"metadata": None,
"file_path": "pending_doc.pdf",
}
],
@ -423,7 +438,10 @@ class DocsStatusesResponse(BaseModel):
"status": "PROCESSED",
"created_at": "2025-03-31T09:00:00",
"updated_at": "2025-03-31T09:05:00",
"track_id": "insert_20250331_090000_def456",
"chunks_count": 8,
"error": None,
"metadata": {"author": "John Doe"},
"file_path": "processed_doc.pdf",
}
],
@ -432,6 +450,192 @@ class DocsStatusesResponse(BaseModel):
}
class TrackStatusResponse(BaseModel):
"""Response model for tracking document processing status by track_id
Attributes:
track_id: The tracking ID
documents: List of documents associated with this track_id
total_count: Total number of documents for this track_id
status_summary: Count of documents by status
"""
track_id: str = Field(description="The tracking ID")
documents: List[DocStatusResponse] = Field(
description="List of documents associated with this track_id"
)
total_count: int = Field(description="Total number of documents for this track_id")
status_summary: Dict[str, int] = Field(description="Count of documents by status")
class Config:
json_schema_extra = {
"example": {
"track_id": "upload_20250729_170612_abc123",
"documents": [
{
"id": "doc_123456",
"content_summary": "Research paper on machine learning",
"content_length": 15240,
"status": "PROCESSED",
"created_at": "2025-03-31T12:34:56",
"updated_at": "2025-03-31T12:35:30",
"track_id": "upload_20250729_170612_abc123",
"chunks_count": 12,
"error": None,
"metadata": {"author": "John Doe", "year": 2025},
"file_path": "research_paper.pdf",
}
],
"total_count": 1,
"status_summary": {"PROCESSED": 1},
}
}
class DocumentsRequest(BaseModel):
"""Request model for paginated document queries
Attributes:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
sort_direction: Sort direction ('asc' or 'desc')
"""
status_filter: Optional[DocStatus] = Field(
default=None, description="Filter by document status, None for all statuses"
)
page: int = Field(default=1, ge=1, description="Page number (1-based)")
page_size: int = Field(
default=50, ge=10, le=200, description="Number of documents per page (10-200)"
)
sort_field: Literal["created_at", "updated_at", "id", "file_path"] = Field(
default="updated_at", description="Field to sort by"
)
sort_direction: Literal["asc", "desc"] = Field(
default="desc", description="Sort direction"
)
class Config:
json_schema_extra = {
"example": {
"status_filter": "PROCESSED",
"page": 1,
"page_size": 50,
"sort_field": "updated_at",
"sort_direction": "desc",
}
}
class PaginationInfo(BaseModel):
"""Pagination information
Attributes:
page: Current page number
page_size: Number of items per page
total_count: Total number of items
total_pages: Total number of pages
has_next: Whether there is a next page
has_prev: Whether there is a previous page
"""
page: int = Field(description="Current page number")
page_size: int = Field(description="Number of items per page")
total_count: int = Field(description="Total number of items")
total_pages: int = Field(description="Total number of pages")
has_next: bool = Field(description="Whether there is a next page")
has_prev: bool = Field(description="Whether there is a previous page")
class Config:
json_schema_extra = {
"example": {
"page": 1,
"page_size": 50,
"total_count": 150,
"total_pages": 3,
"has_next": True,
"has_prev": False,
}
}
class PaginatedDocsResponse(BaseModel):
"""Response model for paginated document queries
Attributes:
documents: List of documents for the current page
pagination: Pagination information
status_counts: Count of documents by status for all documents
"""
documents: List[DocStatusResponse] = Field(
description="List of documents for the current page"
)
pagination: PaginationInfo = Field(description="Pagination information")
status_counts: Dict[str, int] = Field(
description="Count of documents by status for all documents"
)
class Config:
json_schema_extra = {
"example": {
"documents": [
{
"id": "doc_123456",
"content_summary": "Research paper on machine learning",
"content_length": 15240,
"status": "PROCESSED",
"created_at": "2025-03-31T12:34:56",
"updated_at": "2025-03-31T12:35:30",
"track_id": "upload_20250729_170612_abc123",
"chunks_count": 12,
"error_msg": None,
"metadata": {"author": "John Doe", "year": 2025},
"file_path": "research_paper.pdf",
}
],
"pagination": {
"page": 1,
"page_size": 50,
"total_count": 150,
"total_pages": 3,
"has_next": True,
"has_prev": False,
},
"status_counts": {
"PENDING": 10,
"PROCESSING": 5,
"PROCESSED": 130,
"FAILED": 5,
},
}
}
class StatusCountsResponse(BaseModel):
"""Response model for document status counts
Attributes:
status_counts: Count of documents by status
"""
status_counts: Dict[str, int] = Field(description="Count of documents by status")
class Config:
json_schema_extra = {
"example": {
"status_counts": {
"PENDING": 10,
"PROCESSING": 5,
"PROCESSED": 130,
"FAILED": 5,
}
}
}
class PipelineStatusResponse(BaseModel):
"""Response model for pipeline status
@ -549,14 +753,17 @@ class DocumentManager:
return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
async def pipeline_enqueue_file(
rag: LightRAG, file_path: Path, track_id: str = None
) -> tuple[bool, str]:
"""Add a file to the queue for processing
Args:
rag: LightRAG instance
file_path: Path to the saved file
track_id: Optional tracking ID, if not provided will be generated
Returns:
bool: True if the file was successfully enqueued, False otherwise
tuple: (success: bool, track_id: str)
"""
try:
@ -730,9 +937,16 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
f"File contains only whitespace characters. file_paths={file_path.name}"
)
await rag.apipeline_enqueue_documents(content, file_paths=file_path.name)
# Generate track_id if not provided
if track_id is None:
track_id = generate_track_id("unkown")
await rag.apipeline_enqueue_documents(
content, file_paths=file_path.name, track_id=track_id
)
logger.info(f"Successfully fetched and enqueued file: {file_path.name}")
return True
return True, track_id
else:
logger.error(f"No content could be extracted from file: {file_path.name}")
@ -745,18 +959,22 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
file_path.unlink()
except Exception as e:
logger.error(f"Error deleting file {file_path}: {str(e)}")
return False
return False, ""
async def pipeline_index_file(rag: LightRAG, file_path: Path):
"""Index a file
async def pipeline_index_file(rag: LightRAG, file_path: Path, track_id: str = None):
"""Index a file with track_id
Args:
rag: LightRAG instance
file_path: Path to the saved file
track_id: Optional tracking ID
"""
try:
if await pipeline_enqueue_file(rag, file_path):
success, returned_track_id = await pipeline_enqueue_file(
rag, file_path, track_id
)
if success:
await rag.apipeline_process_enqueue_documents()
except Exception as e:
@ -764,12 +982,15 @@ async def pipeline_index_file(rag: LightRAG, file_path: Path):
logger.error(traceback.format_exc())
async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
async def pipeline_index_files(
rag: LightRAG, file_paths: List[Path], track_id: str = None
):
"""Index multiple files sequentially to avoid high CPU load
Args:
rag: LightRAG instance
file_paths: Paths to the files to index
track_id: Optional tracking ID to pass to all files
"""
if not file_paths:
return
@ -780,9 +1001,10 @@ async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
collator = Collator()
sorted_file_paths = sorted(file_paths, key=lambda p: collator.sort_key(str(p)))
# Process files sequentially
# Process files sequentially with track_id
for file_path in sorted_file_paths:
if await pipeline_enqueue_file(rag, file_path):
success, _ = await pipeline_enqueue_file(rag, file_path, track_id)
if success:
enqueued = True
# Process the queue only if at least one file was successfully enqueued
@ -794,14 +1016,18 @@ async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
async def pipeline_index_texts(
rag: LightRAG, texts: List[str], file_sources: List[str] = None
rag: LightRAG,
texts: List[str],
file_sources: List[str] = None,
track_id: str = None,
):
"""Index a list of texts
"""Index a list of texts with track_id
Args:
rag: LightRAG instance
texts: The texts to index
file_sources: Sources of the texts
track_id: Optional tracking ID
"""
if not texts:
return
@ -811,36 +1037,22 @@ async def pipeline_index_texts(
file_sources.append("unknown_source")
for _ in range(len(file_sources), len(texts))
]
await rag.apipeline_enqueue_documents(input=texts, file_paths=file_sources)
await rag.apipeline_enqueue_documents(
input=texts, file_paths=file_sources, track_id=track_id
)
await rag.apipeline_process_enqueue_documents()
# TODO: deprecate after /insert_file is removed
async def save_temp_file(input_dir: Path, file: UploadFile = File(...)) -> Path:
"""Save the uploaded file to a temporary location
async def run_scanning_process(
rag: LightRAG, doc_manager: DocumentManager, track_id: str = None
):
"""Background task to scan and index documents
Args:
file: The uploaded file
Returns:
Path: The path to the saved file
rag: LightRAG instance
doc_manager: DocumentManager instance
track_id: Optional tracking ID to pass to all scanned files
"""
# Generate unique filename to avoid conflicts
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_filename = f"{temp_prefix}{timestamp}_{file.filename}"
# Create a temporary file to save the uploaded content
temp_path = input_dir / "temp" / unique_filename
temp_path.parent.mkdir(exist_ok=True)
# Save the file
with open(temp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return temp_path
async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
"""Background task to scan and index documents"""
try:
new_files = doc_manager.scan_directory_for_new_files()
total_files = len(new_files)
@ -849,8 +1061,8 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
if not new_files:
return
# Process all files at once
await pipeline_index_files(rag, new_files)
# Process all files at once with track_id
await pipeline_index_files(rag, new_files, track_id)
logger.info(f"Scanning process completed: {total_files} files Processed.")
except Exception as e:
@ -1031,13 +1243,17 @@ def create_document_routes(
that fact.
Returns:
ScanResponse: A response object containing the scanning status
ScanResponse: A response object containing the scanning status and track_id
"""
# Start the scanning process in the background
background_tasks.add_task(run_scanning_process, rag, doc_manager)
# Generate track_id with "scan" prefix for scanning operation
track_id = generate_track_id("scan")
# Start the scanning process in the background with track_id
background_tasks.add_task(run_scanning_process, rag, doc_manager, track_id)
return ScanResponse(
status="scanning_started",
message="Scanning process has been initiated in the background",
track_id=track_id,
)
@router.post(
@ -1080,18 +1296,23 @@ def create_document_routes(
return InsertResponse(
status="duplicated",
message=f"File '{safe_filename}' already exists in the input directory.",
track_id="",
)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Add to background tasks
background_tasks.add_task(pipeline_index_file, rag, file_path)
track_id = generate_track_id("upload")
# Add to background tasks and get track_id
background_tasks.add_task(pipeline_index_file, rag, file_path, track_id)
return InsertResponse(
status="success",
message=f"File '{safe_filename}' uploaded successfully. Processing will continue in background.",
track_id=track_id,
)
except Exception as e:
logger.error(f"Error /documents/upload: {file.filename}: {str(e)}")
logger.error(traceback.format_exc())
@ -1120,15 +1341,21 @@ def create_document_routes(
HTTPException: If an error occurs during text processing (500).
"""
try:
# Generate track_id for text insertion
track_id = generate_track_id("insert")
background_tasks.add_task(
pipeline_index_texts,
rag,
[request.text],
file_sources=[request.file_source],
track_id=track_id,
)
return InsertResponse(
status="success",
message="Text successfully received. Processing will continue in background.",
track_id=track_id,
)
except Exception as e:
logger.error(f"Error /documents/text: {str(e)}")
@ -1160,18 +1387,24 @@ def create_document_routes(
HTTPException: If an error occurs during text processing (500).
"""
try:
# Generate track_id for texts insertion
track_id = generate_track_id("insert")
background_tasks.add_task(
pipeline_index_texts,
rag,
request.texts,
file_sources=request.file_sources,
track_id=track_id,
)
return InsertResponse(
status="success",
message="Text successfully received. Processing will continue in background.",
message="Texts successfully received. Processing will continue in background.",
track_id=track_id,
)
except Exception as e:
logger.error(f"Error /documents/text: {str(e)}")
logger.error(f"Error /documents/texts: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@ -1473,8 +1706,9 @@ def create_document_routes(
status=doc_status.status,
created_at=format_datetime(doc_status.created_at),
updated_at=format_datetime(doc_status.updated_at),
track_id=doc_status.track_id,
chunks_count=doc_status.chunks_count,
error=doc_status.error,
error_msg=doc_status.error_msg,
metadata=doc_status.metadata,
file_path=doc_status.file_path,
)
@ -1699,4 +1933,192 @@ def create_document_routes(
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=error_msg)
@router.get(
"/track_status/{track_id}",
response_model=TrackStatusResponse,
dependencies=[Depends(combined_auth)],
)
async def get_track_status(track_id: str) -> TrackStatusResponse:
"""
Get the processing status of documents by tracking ID.
This endpoint retrieves all documents associated with a specific tracking ID,
allowing users to monitor the processing progress of their uploaded files or inserted texts.
Args:
track_id (str): The tracking ID returned from upload, text, or texts endpoints
Returns:
TrackStatusResponse: A response object containing:
- track_id: The tracking ID
- documents: List of documents associated with this track_id
- total_count: Total number of documents for this track_id
Raises:
HTTPException: If track_id is invalid (400) or an error occurs (500).
"""
try:
# Validate track_id
if not track_id or not track_id.strip():
raise HTTPException(status_code=400, detail="Track ID cannot be empty")
track_id = track_id.strip()
# Get documents by track_id
docs_by_track_id = await rag.aget_docs_by_track_id(track_id)
# Convert to response format
documents = []
status_summary = {}
for doc_id, doc_status in docs_by_track_id.items():
documents.append(
DocStatusResponse(
id=doc_id,
content_summary=doc_status.content_summary,
content_length=doc_status.content_length,
status=doc_status.status,
created_at=format_datetime(doc_status.created_at),
updated_at=format_datetime(doc_status.updated_at),
track_id=doc_status.track_id,
chunks_count=doc_status.chunks_count,
error_msg=doc_status.error_msg,
metadata=doc_status.metadata,
file_path=doc_status.file_path,
)
)
# Build status summary
# Handle both DocStatus enum and string cases for robust deserialization
status_key = str(doc_status.status)
status_summary[status_key] = status_summary.get(status_key, 0) + 1
return TrackStatusResponse(
track_id=track_id,
documents=documents,
total_count=len(documents),
status_summary=status_summary,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting track status for {track_id}: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/paginated",
response_model=PaginatedDocsResponse,
dependencies=[Depends(combined_auth)],
)
async def get_documents_paginated(
request: DocumentsRequest,
) -> PaginatedDocsResponse:
"""
Get documents with pagination support.
This endpoint retrieves documents with pagination, filtering, and sorting capabilities.
It provides better performance for large document collections by loading only the
requested page of data.
Args:
request (DocumentsRequest): The request body containing pagination parameters
Returns:
PaginatedDocsResponse: A response object containing:
- documents: List of documents for the current page
- pagination: Pagination information (page, total_count, etc.)
- status_counts: Count of documents by status for all documents
Raises:
HTTPException: If an error occurs while retrieving documents (500).
"""
try:
# Get paginated documents and status counts in parallel
docs_task = rag.doc_status.get_docs_paginated(
status_filter=request.status_filter,
page=request.page,
page_size=request.page_size,
sort_field=request.sort_field,
sort_direction=request.sort_direction,
)
status_counts_task = rag.doc_status.get_all_status_counts()
# Execute both queries in parallel
(documents_with_ids, total_count), status_counts = await asyncio.gather(
docs_task, status_counts_task
)
# Convert documents to response format
doc_responses = []
for doc_id, doc in documents_with_ids:
doc_responses.append(
DocStatusResponse(
id=doc_id,
content_summary=doc.content_summary,
content_length=doc.content_length,
status=doc.status,
created_at=format_datetime(doc.created_at),
updated_at=format_datetime(doc.updated_at),
track_id=doc.track_id,
chunks_count=doc.chunks_count,
error_msg=doc.error_msg,
metadata=doc.metadata,
file_path=doc.file_path,
)
)
# Calculate pagination info
total_pages = (total_count + request.page_size - 1) // request.page_size
has_next = request.page < total_pages
has_prev = request.page > 1
pagination = PaginationInfo(
page=request.page,
page_size=request.page_size,
total_count=total_count,
total_pages=total_pages,
has_next=has_next,
has_prev=has_prev,
)
return PaginatedDocsResponse(
documents=doc_responses,
pagination=pagination,
status_counts=status_counts,
)
except Exception as e:
logger.error(f"Error getting paginated documents: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/status_counts",
response_model=StatusCountsResponse,
dependencies=[Depends(combined_auth)],
)
async def get_document_status_counts() -> StatusCountsResponse:
"""
Get counts of documents by status.
This endpoint retrieves the count of documents in each processing status
(PENDING, PROCESSING, PROCESSED, FAILED) for all documents in the system.
Returns:
StatusCountsResponse: A response object containing status counts
Raises:
HTTPException: If an error occurs while retrieving status counts (500).
"""
try:
status_counts = await rag.doc_status.get_all_status_counts()
return StatusCountsResponse(status_counts=status_counts)
except Exception as e:
logger.error(f"Error getting document status counts: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
return router

View File

@ -11,7 +11,7 @@ import asyncio
from ascii_colors import trace_exception
from lightrag import LightRAG, QueryParam
from lightrag.utils import TiktokenTokenizer
from lightrag.api.utils_api import ollama_server_infos, get_combined_auth_dependency
from lightrag.api.utils_api import get_combined_auth_dependency
from fastapi import Depends
@ -221,7 +221,7 @@ def parse_query_mode(query: str) -> tuple[str, SearchMode, bool, Optional[str]]:
class OllamaAPI:
def __init__(self, rag: LightRAG, top_k: int = 60, api_key: Optional[str] = None):
self.rag = rag
self.ollama_server_infos = ollama_server_infos
self.ollama_server_infos = rag.ollama_server_infos
self.top_k = top_k
self.api_key = api_key
self.router = APIRouter(tags=["ollama"])

View File

@ -268,8 +268,6 @@ def display_splash_screen(args: argparse.Namespace) -> None:
ASCIIColors.yellow(f"{args.summary_language}")
ASCIIColors.white(" ├─ Max Parallel Insert: ", end="")
ASCIIColors.yellow(f"{args.max_parallel_insert}")
ASCIIColors.white(" ├─ Max Embed Tokens: ", end="")
ASCIIColors.yellow(f"{args.max_embed_tokens}")
ASCIIColors.white(" ├─ Chunk Size: ", end="")
ASCIIColors.yellow(f"{args.chunk_size}")
ASCIIColors.white(" ├─ Chunk Overlap Size: ", end="")

View File

@ -1 +1 @@
import{e as v,c as b,g as m,k as O,h as P,j as p,l as w,m as c,n as x,t as A,o as N}from"./_baseUniq-BmMh76ze.js";import{aU as g,aq as _,aV as $,aW as E,aX as F,aY as I,aZ as M,a_ as y,a$ as B,b0 as T}from"./mermaid-vendor-CY273lNM.js";var S=/\s/;function q(n){for(var r=n.length;r--&&S.test(n.charAt(r)););return r}var G=/^\s+/;function H(n){return n&&n.slice(0,q(n)+1).replace(G,"")}var o=NaN,L=/^[-+]0x[0-9a-f]+$/i,R=/^0b[01]+$/i,W=/^0o[0-7]+$/i,X=parseInt;function Y(n){if(typeof n=="number")return n;if(v(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=H(n);var t=R.test(n);return t||W.test(n)?X(n.slice(2),t?2:8):L.test(n)?o:+n}var z=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=Y(n),n===z||n===-1/0){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?b(n):[]}var l=Object.prototype,Z=l.hasOwnProperty,dn=_(function(n,r){n=Object(n);var t=-1,e=r.length,a=e>2?r[2]:void 0;for(a&&$(r[0],r[1],a)&&(e=1);++t<e;)for(var f=r[t],i=E(f),s=-1,d=i.length;++s<d;){var u=i[s],h=n[u];(h===void 0||F(h,l[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,e){var a=Object(r);if(!I(r)){var f=m(t);r=O(r),t=function(s){return f(a[s],s,a)}}var i=n(r,t,e);return i>-1?a[f?r[i]:i]:void 0}}var J=Math.max;function Q(n,r,t){var e=n==null?0:n.length;if(!e)return-1;var a=t==null?0:U(t);return a<0&&(a=J(e+a,0)),P(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,e=I(n)?Array(n.length):[];return p(n,function(a,f,i){e[++t]=r(a,f,i)}),e}function gn(n,r){var t=M(n)?w:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&c(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var e=-1,a=n.length;++e<a;){var f=n[e],i=r(f);if(i!=null&&(s===void 0?i===i&&!v(i):t(i,s)))var s=i,d=f}return d}function on(n){return n&&n.length?tn(n,y,rn):void 0}function an(n,r,t,e){if(!g(n))return n;r=x(r,n);for(var a=-1,f=r.length,i=f-1,s=n;s!=null&&++a<f;){var d=A(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=i){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:B(r[a+1])?[]:{})}T(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var e=-1,a=r.length,f={};++e<a;){var i=r[e],s=N(n,i);t(s,i)&&an(f,x(i,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
import{e as v,c as b,g as m,k as O,h as P,j as p,l as w,m as c,n as x,t as A,o as N}from"./_baseUniq-D-427rzS.js";import{aU as g,aq as _,aV as $,aW as E,aX as F,aY as I,aZ as M,a_ as y,a$ as B,b0 as T}from"./mermaid-vendor-DGPC_TDM.js";var S=/\s/;function q(n){for(var r=n.length;r--&&S.test(n.charAt(r)););return r}var G=/^\s+/;function H(n){return n&&n.slice(0,q(n)+1).replace(G,"")}var o=NaN,L=/^[-+]0x[0-9a-f]+$/i,R=/^0b[01]+$/i,W=/^0o[0-7]+$/i,X=parseInt;function Y(n){if(typeof n=="number")return n;if(v(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=H(n);var t=R.test(n);return t||W.test(n)?X(n.slice(2),t?2:8):L.test(n)?o:+n}var z=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=Y(n),n===z||n===-1/0){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?b(n):[]}var l=Object.prototype,Z=l.hasOwnProperty,dn=_(function(n,r){n=Object(n);var t=-1,e=r.length,a=e>2?r[2]:void 0;for(a&&$(r[0],r[1],a)&&(e=1);++t<e;)for(var f=r[t],i=E(f),s=-1,d=i.length;++s<d;){var u=i[s],h=n[u];(h===void 0||F(h,l[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,e){var a=Object(r);if(!I(r)){var f=m(t);r=O(r),t=function(s){return f(a[s],s,a)}}var i=n(r,t,e);return i>-1?a[f?r[i]:i]:void 0}}var J=Math.max;function Q(n,r,t){var e=n==null?0:n.length;if(!e)return-1;var a=t==null?0:U(t);return a<0&&(a=J(e+a,0)),P(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,e=I(n)?Array(n.length):[];return p(n,function(a,f,i){e[++t]=r(a,f,i)}),e}function gn(n,r){var t=M(n)?w:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&c(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var e=-1,a=n.length;++e<a;){var f=n[e],i=r(f);if(i!=null&&(s===void 0?i===i&&!v(i):t(i,s)))var s=i,d=f}return d}function on(n){return n&&n.length?tn(n,y,rn):void 0}function an(n,r,t,e){if(!g(n))return n;r=x(r,n);for(var a=-1,f=r.length,i=f-1,s=n;s!=null&&++a<f;){var d=A(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=i){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:B(r[a+1])?[]:{})}T(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var e=-1,a=r.length,f={};++e<a;){var i=r[e],s=N(n,i);t(s,i)&&an(f,x(i,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{_ as l}from"./mermaid-vendor-CY273lNM.js";function m(e,c){var i,t,o;e.accDescr&&((i=c.setAccDescription)==null||i.call(c,e.accDescr)),e.accTitle&&((t=c.setAccTitle)==null||t.call(c,e.accTitle)),e.title&&((o=c.setDiagramTitle)==null||o.call(c,e.title))}l(m,"populateCommonDb");export{m as p};
import{_ as l}from"./mermaid-vendor-DGPC_TDM.js";function m(e,c){var i,t,o;e.accDescr&&((i=c.setAccDescription)==null||i.call(c,e.accDescr)),e.accTitle&&((t=c.setAccTitle)==null||t.call(c,e.accTitle)),e.title&&((o=c.setDiagramTitle)==null||o.call(c,e.title))}l(m,"populateCommonDb");export{m as p};

View File

@ -1 +1 @@
import{_ as n,a1 as x,j as l}from"./mermaid-vendor-CY273lNM.js";var c=n((a,t)=>{const e=a.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((a,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};c(a,e).lower()},"drawBackgroundRect"),g=n((a,t)=>{const e=t.text.replace(x," "),r=a.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const s=r.append("tspan");return s.attr("x",t.x+t.textMargin*2),s.text(e),r},"drawText"),h=n((a,t,e,r)=>{const s=a.append("image");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",i)},"drawImage"),m=n((a,t,e,r)=>{const s=a.append("use");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,c as d,h as e,g as f,y as g};
import{_ as n,a1 as x,j as l}from"./mermaid-vendor-DGPC_TDM.js";var c=n((a,t)=>{const e=a.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((a,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};c(a,e).lower()},"drawBackgroundRect"),g=n((a,t)=>{const e=t.text.replace(x," "),r=a.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const s=r.append("tspan");return s.attr("x",t.x+t.textMargin*2),s.text(e),r},"drawText"),h=n((a,t,e,r)=>{const s=a.append("image");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",i)},"drawImage"),m=n((a,t,e,r)=>{const s=a.append("use");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,c as d,h as e,g as f,y as g};

View File

@ -1 +1 @@
import{_ as n,d as r,e as d,l as g}from"./mermaid-vendor-CY273lNM.js";var u=n((e,t)=>{let o;return t==="sandbox"&&(o=r("#i"+e)),(t==="sandbox"?r(o.nodes()[0].contentDocument.body):r("body")).select(`[id="${e}"]`)},"getDiagramElement"),b=n((e,t,o,i)=>{e.attr("class",o);const{width:a,height:s,x:h,y:x}=l(e,t);d(e,s,a,i);const c=w(h,x,a,s,t);e.attr("viewBox",c),g.debug(`viewBox configured: ${c} with padding: ${t}`)},"setupViewPortForSVG"),l=n((e,t)=>{var i;const o=((i=e.node())==null?void 0:i.getBBox())||{width:0,height:0,x:0,y:0};return{width:o.width+t*2,height:o.height+t*2,x:o.x,y:o.y}},"calculateDimensionsWithPadding"),w=n((e,t,o,i,a)=>`${e-a} ${t-a} ${o} ${i}`,"createViewBox");export{u as g,b as s};
import{_ as n,d as r,e as d,l as g}from"./mermaid-vendor-DGPC_TDM.js";var u=n((e,t)=>{let o;return t==="sandbox"&&(o=r("#i"+e)),(t==="sandbox"?r(o.nodes()[0].contentDocument.body):r("body")).select(`[id="${e}"]`)},"getDiagramElement"),b=n((e,t,o,i)=>{e.attr("class",o);const{width:a,height:s,x:h,y:x}=l(e,t);d(e,s,a,i);const c=w(h,x,a,s,t);e.attr("viewBox",c),g.debug(`viewBox configured: ${c} with padding: ${t}`)},"setupViewPortForSVG"),l=n((e,t)=>{var i;const o=((i=e.node())==null?void 0:i.getBBox())||{width:0,height:0,x:0,y:0};return{width:o.width+t*2,height:o.height+t*2,x:o.x,y:o.y}},"calculateDimensionsWithPadding"),w=n((e,t,o,i,a)=>`${e-a} ${t-a} ${o} ${i}`,"createViewBox");export{u as g,b as s};

View File

@ -1 +1 @@
import{_ as s}from"./mermaid-vendor-CY273lNM.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};
import{_ as s}from"./mermaid-vendor-DGPC_TDM.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};

View File

@ -1 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-BECeRkdd.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-ZI6SIAYy.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};

View File

@ -1 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-BECeRkdd.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};
import{s as a,c as s,a as e,C as t}from"./chunk-A2AXSNBT-ZI6SIAYy.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};

View File

@ -0,0 +1 @@
import{b as r}from"./_baseUniq-D-427rzS.js";var e=4;function a(o){return r(o,e)}export{a as c};

View File

@ -1 +0,0 @@
import{b as r}from"./_baseUniq-BmMh76ze.js";var e=4;function a(o){return r(o,e)}export{a as c};

View File

@ -1,4 +1,4 @@
import{p as k}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as l,s as R,g as F,t as I,q as _,a as E,b as D,K as G,z,F as y,G as C,H as P,l as H,Q as V}from"./mermaid-vendor-CY273lNM.js";import{p as W}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},g=structuredClone(w),B=P.radar,j=l(()=>y({...B,...C().radar}),"getConfig"),b=l(()=>g.axes,"getAxes"),q=l(()=>g.curves,"getCurves"),K=l(()=>g.options,"getOptions"),N=l(a=>{g.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Q=l(a=>{g.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:U(t.entries)}))},"setCurves"),U=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>{var o;return((o=s.axis)==null?void 0:o.$refText)===e.name});if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),X=l(a=>{var e,r,s,o,i;const t=a.reduce((n,c)=>(n[c.name]=c,n),{});g.options={showLegend:((e=t.showLegend)==null?void 0:e.value)??h.showLegend,ticks:((r=t.ticks)==null?void 0:r.value)??h.ticks,max:((s=t.max)==null?void 0:s.value)??h.max,min:((o=t.min)==null?void 0:o.value)??h.min,graticule:((i=t.graticule)==null?void 0:i.value)??h.graticule}},"setOptions"),Y=l(()=>{z(),g=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:Q,setOptions:X,getConfig:j,clear:Y,setAccTitle:D,getAccTitle:E,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Z=l(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),J={parse:l(async a=>{const t=await W("radar",a);H.debug(t),Z(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=G(t),p=et(u,c),m=n.max??Math.max(...i.map(f=>Math.max(...f.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,m,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,m=n*Math.cos(p),x=n*Math.sin(p);return`${m},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((m,x)=>{const v=2*Math.PI*x/n-Math.PI/2,f=A(m,r,s,c),O=f*Math.cos(v),S=f*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(m=>`${m.x},${m.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
import{p as k}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as l,s as R,g as F,t as I,q as _,a as E,b as D,K as G,z,F as y,G as C,H as P,l as H,Q as V}from"./mermaid-vendor-DGPC_TDM.js";import{p as W}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var h={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:h},g=structuredClone(w),B=P.radar,j=l(()=>y({...B,...C().radar}),"getConfig"),b=l(()=>g.axes,"getAxes"),q=l(()=>g.curves,"getCurves"),K=l(()=>g.options,"getOptions"),N=l(a=>{g.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Q=l(a=>{g.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:U(t.entries)}))},"setCurves"),U=l(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>{var o;return((o=s.axis)==null?void 0:o.$refText)===e.name});if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),X=l(a=>{var e,r,s,o,i;const t=a.reduce((n,c)=>(n[c.name]=c,n),{});g.options={showLegend:((e=t.showLegend)==null?void 0:e.value)??h.showLegend,ticks:((r=t.ticks)==null?void 0:r.value)??h.ticks,max:((s=t.max)==null?void 0:s.value)??h.max,min:((o=t.min)==null?void 0:o.value)??h.min,graticule:((i=t.graticule)==null?void 0:i.value)??h.graticule}},"setOptions"),Y=l(()=>{z(),g=structuredClone(w)},"clear"),$={getAxes:b,getCurves:q,getOptions:K,setAxes:N,setCurves:Q,setOptions:X,getConfig:j,clear:Y,setAccTitle:D,getAccTitle:E,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Z=l(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),J={parse:l(async a=>{const t=await W("radar",a);H.debug(t),Z(t)},"parse")},tt=l((a,t,e,r)=>{const s=r.db,o=s.getAxes(),i=s.getCurves(),n=s.getOptions(),c=s.getConfig(),d=s.getDiagramTitle(),u=G(t),p=et(u,c),m=n.max??Math.max(...i.map(f=>Math.max(...f.entries))),x=n.min,v=Math.min(c.width,c.height)/2;at(p,o,v,n.ticks,n.graticule),rt(p,o,v,c),M(p,o,i,x,m,n.graticule,c),T(p,i,n.showLegend,c),p.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-c.height/2-c.marginTop)},"draw"),et=l((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return a.attr("viewbox",`0 0 ${e} ${r}`).attr("width",e).attr("height",r),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),at=l((a,t,e,r,s)=>{if(s==="circle")for(let o=0;o<r;o++){const i=e*(o+1)/r;a.append("circle").attr("r",i).attr("class","radarGraticule")}else if(s==="polygon"){const o=t.length;for(let i=0;i<r;i++){const n=e*(i+1)/r,c=t.map((d,u)=>{const p=2*u*Math.PI/o-Math.PI/2,m=n*Math.cos(p),x=n*Math.sin(p);return`${m},${x}`}).join(" ");a.append("polygon").attr("points",c).attr("class","radarGraticule")}}},"drawGraticule"),rt=l((a,t,e,r)=>{const s=t.length;for(let o=0;o<s;o++){const i=t[o].label,n=2*o*Math.PI/s-Math.PI/2;a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*Math.cos(n)).attr("y2",e*r.axisScaleFactor*Math.sin(n)).attr("class","radarAxisLine"),a.append("text").text(i).attr("x",e*r.axisLabelFactor*Math.cos(n)).attr("y",e*r.axisLabelFactor*Math.sin(n)).attr("class","radarAxisLabel")}},"drawAxes");function M(a,t,e,r,s,o,i){const n=t.length,c=Math.min(i.width,i.height)/2;e.forEach((d,u)=>{if(d.entries.length!==n)return;const p=d.entries.map((m,x)=>{const v=2*Math.PI*x/n-Math.PI/2,f=A(m,r,s,c),O=f*Math.cos(v),S=f*Math.sin(v);return{x:O,y:S}});o==="circle"?a.append("path").attr("d",L(p,i.curveTension)).attr("class",`radarCurve-${u}`):o==="polygon"&&a.append("polygon").attr("points",p.map(m=>`${m.x},${m.y}`).join(" ")).attr("class",`radarCurve-${u}`)})}l(M,"drawCurves");function A(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}l(A,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const o=a[(s-1+e)%e],i=a[s],n=a[(s+1)%e],c=a[(s+2)%e],d={x:i.x+(n.x-o.x)*t,y:i.y+(n.y-o.y)*t},u={x:n.x-(c.x-i.x)*t,y:n.y-(c.y-i.y)*t};r+=` C${d.x},${d.y} ${u.x},${u.y} ${n.x},${n.y}`}return`${r} Z`}l(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,o=-(r.height/2+r.marginTop)*3/4,i=20;t.forEach((n,c)=>{const d=a.append("g").attr("transform",`translate(${s}, ${o+c*i})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${c}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}l(T,"drawLegend");var st={draw:tt},nt=l((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
.radarCurve-${r} {
color: ${s};
fill: ${s};

View File

@ -1,4 +1,4 @@
import{p as w}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as n,s as B,g as S,t as F,q as z,a as P,b as W,F as x,K as T,e as D,z as _,G as A,H as E,l as v}from"./mermaid-vendor-CY273lNM.js";import{p as N}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var C={packet:[]},h=structuredClone(C),L=E.packet,Y=n(()=>{const t=x({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t},"getConfig"),G=n(()=>h.packet,"getPacket"),H=n(t=>{t.length>0&&h.packet.push(t)},"pushWord"),I=n(()=>{_(),h=structuredClone(C)},"clear"),m={pushWord:H,getPacket:G,getConfig:Y,clear:I,setAccTitle:W,getAccTitle:P,setDiagramTitle:z,getDiagramTitle:F,getAccDescription:S,setAccDescription:B},K=1e4,M=n(t=>{w(t,m);let e=-1,o=[],s=1;const{bitsPerRow:i}=m.getConfig();for(let{start:a,end:r,label:p}of t.blocks){if(r&&r<a)throw new Error(`Packet block ${a} - ${r} is invalid. End must be greater than start.`);if(a!==e+1)throw new Error(`Packet block ${a} - ${r??a} is not contiguous. It should start from ${e+1}.`);for(e=r??a,v.debug(`Packet block ${a} - ${e} with label ${p}`);o.length<=i+1&&m.getPacket().length<K;){const[b,c]=O({start:a,end:r,label:p},s,i);if(o.push(b),b.end+1===s*i&&(m.pushWord(o),o=[],s++),!c)break;({start:a,end:r,label:p}=c)}}m.pushWord(o)},"populate"),O=n((t,e,o)=>{if(t.end===void 0&&(t.end=t.start),t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);return t.end+1<=e*o?[t,void 0]:[{start:t.start,end:e*o-1,label:t.label},{start:e*o,end:t.end,label:t.label}]},"getNextFittingBlock"),q={parse:n(async t=>{const e=await N("packet",t);v.debug(e),M(e)},"parse")},R=n((t,e,o,s)=>{const i=s.db,a=i.getConfig(),{rowHeight:r,paddingY:p,bitWidth:b,bitsPerRow:c}=a,u=i.getPacket(),l=i.getDiagramTitle(),g=r+p,d=g*(u.length+1)-(l?0:r),k=b*c+2,f=T(e);f.attr("viewbox",`0 0 ${k} ${d}`),D(f,d,k,a.useMaxWidth);for(const[$,y]of u.entries())U(f,y,$,a);f.append("text").text(l).attr("x",k/2).attr("y",d-g/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),U=n((t,e,o,{rowHeight:s,paddingX:i,paddingY:a,bitWidth:r,bitsPerRow:p,showBits:b})=>{const c=t.append("g"),u=o*(s+a)+a;for(const l of e){const g=l.start%p*r+1,d=(l.end-l.start+1)*r-i;if(c.append("rect").attr("x",g).attr("y",u).attr("width",d).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",g+d/2).attr("y",u+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(l.label),!b)continue;const k=l.end===l.start,f=u-2;c.append("text").attr("x",g+(k?d/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(l.start),k||c.append("text").attr("x",g+d).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(l.end)}},"drawWord"),X={draw:R},j={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},J=n(({packet:t}={})=>{const e=x(j,t);return`
import{p as w}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as n,s as B,g as S,t as F,q as z,a as P,b as W,F as x,K as T,e as D,z as _,G as A,H as E,l as v}from"./mermaid-vendor-DGPC_TDM.js";import{p as N}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var C={packet:[]},h=structuredClone(C),L=E.packet,Y=n(()=>{const t=x({...L,...A().packet});return t.showBits&&(t.paddingY+=10),t},"getConfig"),G=n(()=>h.packet,"getPacket"),H=n(t=>{t.length>0&&h.packet.push(t)},"pushWord"),I=n(()=>{_(),h=structuredClone(C)},"clear"),m={pushWord:H,getPacket:G,getConfig:Y,clear:I,setAccTitle:W,getAccTitle:P,setDiagramTitle:z,getDiagramTitle:F,getAccDescription:S,setAccDescription:B},K=1e4,M=n(t=>{w(t,m);let e=-1,o=[],s=1;const{bitsPerRow:i}=m.getConfig();for(let{start:a,end:r,label:p}of t.blocks){if(r&&r<a)throw new Error(`Packet block ${a} - ${r} is invalid. End must be greater than start.`);if(a!==e+1)throw new Error(`Packet block ${a} - ${r??a} is not contiguous. It should start from ${e+1}.`);for(e=r??a,v.debug(`Packet block ${a} - ${e} with label ${p}`);o.length<=i+1&&m.getPacket().length<K;){const[b,c]=O({start:a,end:r,label:p},s,i);if(o.push(b),b.end+1===s*i&&(m.pushWord(o),o=[],s++),!c)break;({start:a,end:r,label:p}=c)}}m.pushWord(o)},"populate"),O=n((t,e,o)=>{if(t.end===void 0&&(t.end=t.start),t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);return t.end+1<=e*o?[t,void 0]:[{start:t.start,end:e*o-1,label:t.label},{start:e*o,end:t.end,label:t.label}]},"getNextFittingBlock"),q={parse:n(async t=>{const e=await N("packet",t);v.debug(e),M(e)},"parse")},R=n((t,e,o,s)=>{const i=s.db,a=i.getConfig(),{rowHeight:r,paddingY:p,bitWidth:b,bitsPerRow:c}=a,u=i.getPacket(),l=i.getDiagramTitle(),g=r+p,d=g*(u.length+1)-(l?0:r),k=b*c+2,f=T(e);f.attr("viewbox",`0 0 ${k} ${d}`),D(f,d,k,a.useMaxWidth);for(const[$,y]of u.entries())U(f,y,$,a);f.append("text").text(l).attr("x",k/2).attr("y",d-g/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),U=n((t,e,o,{rowHeight:s,paddingX:i,paddingY:a,bitWidth:r,bitsPerRow:p,showBits:b})=>{const c=t.append("g"),u=o*(s+a)+a;for(const l of e){const g=l.start%p*r+1,d=(l.end-l.start+1)*r-i;if(c.append("rect").attr("x",g).attr("y",u).attr("width",d).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",g+d/2).attr("y",u+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(l.label),!b)continue;const k=l.end===l.start,f=u-2;c.append("text").attr("x",g+(k?d/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(l.start),k||c.append("text").attr("x",g+d).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(l.end)}},"drawWord"),X={draw:R},j={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},J=n(({packet:t}={})=>{const e=x(j,t);return`
.packetByte {
font-size: ${e.byteFontSize};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import{_ as m,o as O1,l as Z,c as Ge,d as Ce,p as H1,r as q1,u as i1,b as X1,s as Q1,q as J1,a as Z1,g as $1,t as et,k as tt,v as st,J as it,x as rt,y as s1,z as nt,A as at,B as ut,C as lt}from"./mermaid-vendor-CY273lNM.js";import{g as ot,s as ct}from"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var ht="flowchart-",Pe,dt=(Pe=class{constructor(){this.vertexCounter=0,this.config=Ge(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=X1,this.setAccDescription=Q1,this.setDiagramTitle=J1,this.getAccTitle=Z1,this.getAccDescription=$1,this.getDiagramTitle=et,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return tt.sanitizeText(i,this.config)}lookUpDomId(i){for(const n of this.vertices.values())if(n.id===i)return n.domId;return i}addVertex(i,n,a,u,l,f,c={},A){var U,T;if(!i||i.trim().length===0)return;let r;if(A!==void 0){let d;A.includes(`
import{_ as m,o as O1,l as Z,c as Ge,d as Ce,p as H1,r as q1,u as i1,b as X1,s as Q1,q as J1,a as Z1,g as $1,t as et,k as tt,v as st,J as it,x as rt,y as s1,z as nt,A as at,B as ut,C as lt}from"./mermaid-vendor-DGPC_TDM.js";import{g as ot,s as ct}from"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var ht="flowchart-",Pe,dt=(Pe=class{constructor(){this.vertexCounter=0,this.config=Ge(),this.vertices=new Map,this.edges=[],this.classes=new Map,this.subGraphs=[],this.subGraphLookup=new Map,this.tooltips=new Map,this.subCount=0,this.firstGraphFlag=!0,this.secCount=-1,this.posCrossRef=[],this.funs=[],this.setAccTitle=X1,this.setAccDescription=Q1,this.setDiagramTitle=J1,this.getAccTitle=Z1,this.getAccDescription=$1,this.getDiagramTitle=et,this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}sanitizeText(i){return tt.sanitizeText(i,this.config)}lookUpDomId(i){for(const n of this.vertices.values())if(n.id===i)return n.domId;return i}addVertex(i,n,a,u,l,f,c={},A){var U,T;if(!i||i.trim().length===0)return;let r;if(A!==void 0){let d;A.includes(`
`)?d=A+`
`:d=`{
`+A+`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
import{_ as e,l as o,K as i,e as n,L as p}from"./mermaid-vendor-CY273lNM.js";import{p as m}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var g={parse:e(async r=>{const a=await m("info",r);o.debug(a)},"parse")},v={version:p.version},d=e(()=>v.version,"getVersion"),c={getVersion:d},l=e((r,a,s)=>{o.debug(`rendering info diagram
import{_ as e,l as o,K as i,e as n,L as p}from"./mermaid-vendor-DGPC_TDM.js";import{p as m}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var g={parse:e(async r=>{const a=await m("info",r);o.debug(a)},"parse")},v={version:p.version},d=e(()=>v.version,"getVersion"),c={getVersion:d},l=e((r,a,s)=>{o.debug(`rendering info diagram
`+r);const t=i(a);n(t,100,400,!0),t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${s}`)},"draw"),f={draw:l},L={parser:g,db:c,renderer:f};export{L as diagram};

View File

@ -1,4 +1,4 @@
import{a as pt,g as at,f as gt,d as mt}from"./chunk-D6G4REZN-Cg1L9G2i.js";import{_ as s,g as xt,s as kt,a as _t,b as bt,t as vt,q as wt,c as A,d as W,e as Tt,z as St,N as tt}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var H=function(){var t=s(function(g,r,a,l){for(a=a||{},l=g.length;l--;a[g[l]]=r);return a},"o"),e=[6,8,10,11,12,14,16,17,18],i=[1,9],c=[1,10],n=[1,11],u=[1,12],h=[1,13],f=[1,14],d={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:s(function(r,a,l,y,p,o,S){var _=o.length-1;switch(p){case 1:return o[_-1];case 2:this.$=[];break;case 3:o[_-1].push(o[_]),this.$=o[_-1];break;case 4:case 5:this.$=o[_];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(o[_].substr(6)),this.$=o[_].substr(6);break;case 9:this.$=o[_].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=o[_].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(o[_].substr(8)),this.$=o[_].substr(8);break;case 13:y.addTask(o[_-1],o[_]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var l=new Error(r);throw l.hash=a,l}},"parseError"),parse:s(function(r){var a=this,l=[0],y=[],p=[null],o=[],S=this.table,_="",B=0,J=0,ut=2,K=1,yt=o.slice.call(arguments,1),k=Object.create(this.lexer),E={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(E.yy[O]=this.yy[O]);k.setInput(r,E.yy),E.yy.lexer=k,E.yy.parser=this,typeof k.yylloc>"u"&&(k.yylloc={});var Y=k.yylloc;o.push(Y);var dt=k.options&&k.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function ft(v){l.length=l.length-2*v,p.length=p.length-v,o.length=o.length-v}s(ft,"popStack");function Q(){var v;return v=y.pop()||k.lex()||K,typeof v!="number"&&(v instanceof Array&&(y=v,v=y.pop()),v=a.symbols_[v]||v),v}s(Q,"lex");for(var b,P,w,q,C={},N,$,D,j;;){if(P=l[l.length-1],this.defaultActions[P]?w=this.defaultActions[P]:((b===null||typeof b>"u")&&(b=Q()),w=S[P]&&S[P][b]),typeof w>"u"||!w.length||!w[0]){var G="";j=[];for(N in S[P])this.terminals_[N]&&N>ut&&j.push("'"+this.terminals_[N]+"'");k.showPosition?G="Parse error on line "+(B+1)+`:
import{a as pt,g as at,f as gt,d as mt}from"./chunk-D6G4REZN-5bNYmyqV.js";import{_ as s,g as xt,s as kt,a as _t,b as bt,t as vt,q as wt,c as A,d as W,e as Tt,z as St,N as tt}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var H=function(){var t=s(function(g,r,a,l){for(a=a||{},l=g.length;l--;a[g[l]]=r);return a},"o"),e=[6,8,10,11,12,14,16,17,18],i=[1,9],c=[1,10],n=[1,11],u=[1,12],h=[1,13],f=[1,14],d={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:s(function(r,a,l,y,p,o,S){var _=o.length-1;switch(p){case 1:return o[_-1];case 2:this.$=[];break;case 3:o[_-1].push(o[_]),this.$=o[_-1];break;case 4:case 5:this.$=o[_];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(o[_].substr(6)),this.$=o[_].substr(6);break;case 9:this.$=o[_].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=o[_].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(o[_].substr(8)),this.$=o[_].substr(8);break;case 13:y.addTask(o[_-1],o[_]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,7],{1:[2,1]}),t(e,[2,3]),{9:15,11:i,12:c,14:n,16:u,17:h,18:f},t(e,[2,5]),t(e,[2,6]),t(e,[2,8]),{13:[1,16]},{15:[1,17]},t(e,[2,11]),t(e,[2,12]),{19:[1,18]},t(e,[2,4]),t(e,[2,9]),t(e,[2,10]),t(e,[2,13])],defaultActions:{},parseError:s(function(r,a){if(a.recoverable)this.trace(r);else{var l=new Error(r);throw l.hash=a,l}},"parseError"),parse:s(function(r){var a=this,l=[0],y=[],p=[null],o=[],S=this.table,_="",B=0,J=0,ut=2,K=1,yt=o.slice.call(arguments,1),k=Object.create(this.lexer),E={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(E.yy[O]=this.yy[O]);k.setInput(r,E.yy),E.yy.lexer=k,E.yy.parser=this,typeof k.yylloc>"u"&&(k.yylloc={});var Y=k.yylloc;o.push(Y);var dt=k.options&&k.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function ft(v){l.length=l.length-2*v,p.length=p.length-v,o.length=o.length-v}s(ft,"popStack");function Q(){var v;return v=y.pop()||k.lex()||K,typeof v!="number"&&(v instanceof Array&&(y=v,v=y.pop()),v=a.symbols_[v]||v),v}s(Q,"lex");for(var b,P,w,q,C={},N,$,D,j;;){if(P=l[l.length-1],this.defaultActions[P]?w=this.defaultActions[P]:((b===null||typeof b>"u")&&(b=Q()),w=S[P]&&S[P][b]),typeof w>"u"||!w.length||!w[0]){var G="";j=[];for(N in S[P])this.terminals_[N]&&N>ut&&j.push("'"+this.terminals_[N]+"'");k.showPosition?G="Parse error on line "+(B+1)+`:
`+k.showPosition()+`
Expecting `+j.join(", ")+", got '"+(this.terminals_[b]||b)+"'":G="Parse error on line "+(B+1)+": Unexpected "+(b==K?"end of input":"'"+(this.terminals_[b]||b)+"'"),this.parseError(G,{text:k.match,token:this.terminals_[b]||b,line:k.yylineno,loc:Y,expected:j})}if(w[0]instanceof Array&&w.length>1)throw new Error("Parse Error: multiple actions possible at state: "+P+", token: "+b);switch(w[0]){case 1:l.push(b),p.push(k.yytext),o.push(k.yylloc),l.push(w[1]),b=null,J=k.yyleng,_=k.yytext,B=k.yylineno,Y=k.yylloc;break;case 2:if($=this.productions_[w[1]][1],C.$=p[p.length-$],C._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},dt&&(C._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),q=this.performAction.apply(C,[_,J,B,E.yy,w[1],p,o].concat(yt)),typeof q<"u")return q;$&&(l=l.slice(0,-1*$*2),p=p.slice(0,-1*$),o=o.slice(0,-1*$)),l.push(this.productions_[w[1]][0]),p.push(C.$),o.push(C._$),D=S[l[l.length-2]][l[l.length-1]],l.push(D);break;case 3:return!0}}return!0},"parse")},x=function(){var g={EOF:1,parseError:s(function(a,l){if(this.yy.parser)this.yy.parser.parseError(a,l);else throw new Error(a)},"parseError"),setInput:s(function(r,a){return this.yy=a||this.yy||{},this._input=r,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var r=this._input[0];this.yytext+=r,this.yyleng++,this.offset++,this.match+=r,this.matched+=r;var a=r.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),r},"input"),unput:s(function(r){var a=r.length,l=r.split(/(?:\r\n?|\n)/g);this._input=r+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var y=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),l.length-1&&(this.yylineno-=l.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:l?(l.length===y.length?this.yylloc.first_column:0)+y[y.length-l.length].length-l[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(r){this.unput(this.match.slice(r))},"less"),pastInput:s(function(){var r=this.matched.substr(0,this.matched.length-this.match.length);return(r.length>20?"...":"")+r.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var r=this.match;return r.length<20&&(r+=this._input.substr(0,20-r.length)),(r.substr(0,20)+(r.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var r=this.pastInput(),a=new Array(r.length+1).join("-");return r+this.upcomingInput()+`

View File

@ -1,4 +1,4 @@
import{_ as c,l as te,c as W,K as fe,a7 as ye,a8 as be,a9 as me,a2 as _e,H as Y,i as G,v as Ee,J as ke,a3 as Se,a4 as le,a5 as ce}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var $=function(){var t=c(function(_,i,n,a){for(n=n||{},a=_.length;a--;n[_[a]]=i);return n},"o"),g=[1,4],d=[1,13],r=[1,12],p=[1,15],E=[1,16],f=[1,20],h=[1,19],L=[6,7,8],C=[1,26],w=[1,24],N=[1,25],s=[6,7,11],H=[1,31],x=[6,7,11,24],P=[1,6,13,16,17,20,23],M=[1,35],U=[1,36],A=[1,6,7,11,13,16,17,20,23],j=[1,38],V={trace:c(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:c(function(i,n,a,o,u,e,B){var l=e.length-1;switch(u){case 6:case 7:return o;case 8:o.getLogger().trace("Stop NL ");break;case 9:o.getLogger().trace("Stop EOF ");break;case 11:o.getLogger().trace("Stop NL2 ");break;case 12:o.getLogger().trace("Stop EOF2 ");break;case 15:o.getLogger().info("Node: ",e[l-1].id),o.addNode(e[l-2].length,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 16:o.getLogger().info("Node: ",e[l].id),o.addNode(e[l-1].length,e[l].id,e[l].descr,e[l].type);break;case 17:o.getLogger().trace("Icon: ",e[l]),o.decorateNode({icon:e[l]});break;case 18:case 23:o.decorateNode({class:e[l]});break;case 19:o.getLogger().trace("SPACELIST");break;case 20:o.getLogger().trace("Node: ",e[l-1].id),o.addNode(0,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 21:o.getLogger().trace("Node: ",e[l].id),o.addNode(0,e[l].id,e[l].descr,e[l].type);break;case 22:o.decorateNode({icon:e[l]});break;case 27:o.getLogger().trace("node found ..",e[l-2]),this.$={id:e[l-1],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 28:this.$={id:e[l],descr:e[l],type:0};break;case 29:o.getLogger().trace("node found ..",e[l-3]),this.$={id:e[l-3],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 30:this.$=e[l-1]+e[l];break;case 31:this.$=e[l];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:g},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:g},{6:d,7:[1,10],9:9,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(L,[2,3]),{1:[2,2]},t(L,[2,4]),t(L,[2,5]),{1:[2,6],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:d,9:22,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:C,7:w,10:23,11:N},t(s,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:f,23:h}),t(s,[2,19]),t(s,[2,21],{15:30,24:H}),t(s,[2,22]),t(s,[2,23]),t(x,[2,25]),t(x,[2,26]),t(x,[2,28],{20:[1,32]}),{21:[1,33]},{6:C,7:w,10:34,11:N},{1:[2,7],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(P,[2,14],{7:M,11:U}),t(A,[2,8]),t(A,[2,9]),t(A,[2,10]),t(s,[2,16],{15:37,24:H}),t(s,[2,17]),t(s,[2,18]),t(s,[2,20],{24:j}),t(x,[2,31]),{21:[1,39]},{22:[1,40]},t(P,[2,13],{7:M,11:U}),t(A,[2,11]),t(A,[2,12]),t(s,[2,15],{24:j}),t(x,[2,30]),{22:[1,41]},t(x,[2,27]),t(x,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:c(function(i,n){if(n.recoverable)this.trace(i);else{var a=new Error(i);throw a.hash=n,a}},"parseError"),parse:c(function(i){var n=this,a=[0],o=[],u=[null],e=[],B=this.table,l="",z=0,se=0,ue=2,re=1,ge=e.slice.call(arguments,1),b=Object.create(this.lexer),T={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(T.yy[J]=this.yy[J]);b.setInput(i,T.yy),T.yy.lexer=b,T.yy.parser=this,typeof b.yylloc>"u"&&(b.yylloc={});var q=b.yylloc;e.push(q);var de=b.options&&b.options.ranges;typeof T.yy.parseError=="function"?this.parseError=T.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){a.length=a.length-2*S,u.length=u.length-S,e.length=e.length-S}c(pe,"popStack");function ae(){var S;return S=o.pop()||b.lex()||re,typeof S!="number"&&(S instanceof Array&&(o=S,S=o.pop()),S=n.symbols_[S]||S),S}c(ae,"lex");for(var k,R,v,Q,F={},K,I,oe,X;;){if(R=a[a.length-1],this.defaultActions[R]?v=this.defaultActions[R]:((k===null||typeof k>"u")&&(k=ae()),v=B[R]&&B[R][k]),typeof v>"u"||!v.length||!v[0]){var Z="";X=[];for(K in B[R])this.terminals_[K]&&K>ue&&X.push("'"+this.terminals_[K]+"'");b.showPosition?Z="Parse error on line "+(z+1)+`:
import{_ as c,l as te,c as W,K as fe,a7 as ye,a8 as be,a9 as me,a2 as _e,H as Y,i as G,v as Ee,J as ke,a3 as Se,a4 as le,a5 as ce}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var $=function(){var t=c(function(_,i,n,a){for(n=n||{},a=_.length;a--;n[_[a]]=i);return n},"o"),g=[1,4],d=[1,13],r=[1,12],p=[1,15],E=[1,16],f=[1,20],h=[1,19],L=[6,7,8],C=[1,26],w=[1,24],N=[1,25],s=[6,7,11],H=[1,31],x=[6,7,11,24],P=[1,6,13,16,17,20,23],M=[1,35],U=[1,36],A=[1,6,7,11,13,16,17,20,23],j=[1,38],V={trace:c(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:c(function(i,n,a,o,u,e,B){var l=e.length-1;switch(u){case 6:case 7:return o;case 8:o.getLogger().trace("Stop NL ");break;case 9:o.getLogger().trace("Stop EOF ");break;case 11:o.getLogger().trace("Stop NL2 ");break;case 12:o.getLogger().trace("Stop EOF2 ");break;case 15:o.getLogger().info("Node: ",e[l-1].id),o.addNode(e[l-2].length,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 16:o.getLogger().info("Node: ",e[l].id),o.addNode(e[l-1].length,e[l].id,e[l].descr,e[l].type);break;case 17:o.getLogger().trace("Icon: ",e[l]),o.decorateNode({icon:e[l]});break;case 18:case 23:o.decorateNode({class:e[l]});break;case 19:o.getLogger().trace("SPACELIST");break;case 20:o.getLogger().trace("Node: ",e[l-1].id),o.addNode(0,e[l-1].id,e[l-1].descr,e[l-1].type,e[l]);break;case 21:o.getLogger().trace("Node: ",e[l].id),o.addNode(0,e[l].id,e[l].descr,e[l].type);break;case 22:o.decorateNode({icon:e[l]});break;case 27:o.getLogger().trace("node found ..",e[l-2]),this.$={id:e[l-1],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 28:this.$={id:e[l],descr:e[l],type:0};break;case 29:o.getLogger().trace("node found ..",e[l-3]),this.$={id:e[l-3],descr:e[l-1],type:o.getType(e[l-2],e[l])};break;case 30:this.$=e[l-1]+e[l];break;case 31:this.$=e[l];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:g},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:g},{6:d,7:[1,10],9:9,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(L,[2,3]),{1:[2,2]},t(L,[2,4]),t(L,[2,5]),{1:[2,6],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:d,9:22,12:11,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},{6:C,7:w,10:23,11:N},t(s,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:f,23:h}),t(s,[2,19]),t(s,[2,21],{15:30,24:H}),t(s,[2,22]),t(s,[2,23]),t(x,[2,25]),t(x,[2,26]),t(x,[2,28],{20:[1,32]}),{21:[1,33]},{6:C,7:w,10:34,11:N},{1:[2,7],6:d,12:21,13:r,14:14,16:p,17:E,18:17,19:18,20:f,23:h},t(P,[2,14],{7:M,11:U}),t(A,[2,8]),t(A,[2,9]),t(A,[2,10]),t(s,[2,16],{15:37,24:H}),t(s,[2,17]),t(s,[2,18]),t(s,[2,20],{24:j}),t(x,[2,31]),{21:[1,39]},{22:[1,40]},t(P,[2,13],{7:M,11:U}),t(A,[2,11]),t(A,[2,12]),t(s,[2,15],{24:j}),t(x,[2,30]),{22:[1,41]},t(x,[2,27]),t(x,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:c(function(i,n){if(n.recoverable)this.trace(i);else{var a=new Error(i);throw a.hash=n,a}},"parseError"),parse:c(function(i){var n=this,a=[0],o=[],u=[null],e=[],B=this.table,l="",z=0,se=0,ue=2,re=1,ge=e.slice.call(arguments,1),b=Object.create(this.lexer),T={yy:{}};for(var J in this.yy)Object.prototype.hasOwnProperty.call(this.yy,J)&&(T.yy[J]=this.yy[J]);b.setInput(i,T.yy),T.yy.lexer=b,T.yy.parser=this,typeof b.yylloc>"u"&&(b.yylloc={});var q=b.yylloc;e.push(q);var de=b.options&&b.options.ranges;typeof T.yy.parseError=="function"?this.parseError=T.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function pe(S){a.length=a.length-2*S,u.length=u.length-S,e.length=e.length-S}c(pe,"popStack");function ae(){var S;return S=o.pop()||b.lex()||re,typeof S!="number"&&(S instanceof Array&&(o=S,S=o.pop()),S=n.symbols_[S]||S),S}c(ae,"lex");for(var k,R,v,Q,F={},K,I,oe,X;;){if(R=a[a.length-1],this.defaultActions[R]?v=this.defaultActions[R]:((k===null||typeof k>"u")&&(k=ae()),v=B[R]&&B[R][k]),typeof v>"u"||!v.length||!v[0]){var Z="";X=[];for(K in B[R])this.terminals_[K]&&K>ue&&X.push("'"+this.terminals_[K]+"'");b.showPosition?Z="Parse error on line "+(z+1)+`:
`+b.showPosition()+`
Expecting `+X.join(", ")+", got '"+(this.terminals_[k]||k)+"'":Z="Parse error on line "+(z+1)+": Unexpected "+(k==re?"end of input":"'"+(this.terminals_[k]||k)+"'"),this.parseError(Z,{text:b.match,token:this.terminals_[k]||k,line:b.yylineno,loc:q,expected:X})}if(v[0]instanceof Array&&v.length>1)throw new Error("Parse Error: multiple actions possible at state: "+R+", token: "+k);switch(v[0]){case 1:a.push(k),u.push(b.yytext),e.push(b.yylloc),a.push(v[1]),k=null,se=b.yyleng,l=b.yytext,z=b.yylineno,q=b.yylloc;break;case 2:if(I=this.productions_[v[1]][1],F.$=u[u.length-I],F._$={first_line:e[e.length-(I||1)].first_line,last_line:e[e.length-1].last_line,first_column:e[e.length-(I||1)].first_column,last_column:e[e.length-1].last_column},de&&(F._$.range=[e[e.length-(I||1)].range[0],e[e.length-1].range[1]]),Q=this.performAction.apply(F,[l,se,z,T.yy,v[1],u,e].concat(ge)),typeof Q<"u")return Q;I&&(a=a.slice(0,-1*I*2),u=u.slice(0,-1*I),e=e.slice(0,-1*I)),a.push(this.productions_[v[1]][0]),u.push(F.$),e.push(F._$),oe=B[a[a.length-2]][a[a.length-1]],a.push(oe);break;case 3:return!0}}return!0},"parse")},m=function(){var _={EOF:1,parseError:c(function(n,a){if(this.yy.parser)this.yy.parser.parseError(n,a);else throw new Error(n)},"parseError"),setInput:c(function(i,n){return this.yy=n||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:c(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var n=i.match(/(?:\r\n?|\n).*/g);return n?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:c(function(i){var n=i.length,a=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-n),this.offset-=n;var o=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),a.length-1&&(this.yylineno-=a.length-1);var u=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:a?(a.length===o.length?this.yylloc.first_column:0)+o[o.length-a.length].length-a[0].length:this.yylloc.first_column-n},this.options.ranges&&(this.yylloc.range=[u[0],u[0]+this.yyleng-n]),this.yyleng=this.yytext.length,this},"unput"),more:c(function(){return this._more=!0,this},"more"),reject:c(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:c(function(i){this.unput(this.match.slice(i))},"less"),pastInput:c(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:c(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:c(function(){var i=this.pastInput(),n=new Array(i.length+1).join("-");return i+this.upcomingInput()+`

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import{p as N}from"./chunk-4BMEZGHF-DcXQJx5Q.js";import{_ as i,g as B,s as U,a as q,b as H,t as K,q as V,l as C,c as Z,F as j,K as J,M as Q,N as z,O as X,e as Y,z as tt,P as et,H as at}from"./mermaid-vendor-CY273lNM.js";import{p as rt}from"./radar-MK3ICKWK-D66UL2n2.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BmMh76ze.js";import"./_basePickBy-deWrp6cQ.js";import"./clone-DjMnmZIs.js";var it=at.pie,D={sections:new Map,showData:!1},f=D.sections,w=D.showData,st=structuredClone(it),ot=i(()=>structuredClone(st),"getConfig"),nt=i(()=>{f=new Map,w=D.showData,tt()},"clear"),lt=i(({label:t,value:a})=>{f.has(t)||(f.set(t,a),C.debug(`added new section: ${t}, with value: ${a}`))},"addSection"),ct=i(()=>f,"getSections"),pt=i(t=>{w=t},"setShowData"),dt=i(()=>w,"getShowData"),F={getConfig:ot,clear:nt,setDiagramTitle:V,getDiagramTitle:K,setAccTitle:H,getAccTitle:q,setAccDescription:U,getAccDescription:B,addSection:lt,getSections:ct,setShowData:pt,getShowData:dt},gt=i((t,a)=>{N(t,a),a.setShowData(t.showData),t.sections.map(a.addSection)},"populateDb"),ut={parse:i(async t=>{const a=await rt("pie",t);C.debug(a),gt(a,F)},"parse")},mt=i(t=>`
import{p as N}from"./chunk-4BMEZGHF-CbUqLJH_.js";import{_ as i,g as B,s as U,a as q,b as H,t as K,q as V,l as C,c as Z,F as j,K as J,M as Q,N as z,O as X,e as Y,z as tt,P as et,H as at}from"./mermaid-vendor-DGPC_TDM.js";import{p as rt}from"./radar-MK3ICKWK-BkgLvISn.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-D-427rzS.js";import"./_basePickBy-CGXZVaMl.js";import"./clone-BvhE3BKU.js";var it=at.pie,D={sections:new Map,showData:!1},f=D.sections,w=D.showData,st=structuredClone(it),ot=i(()=>structuredClone(st),"getConfig"),nt=i(()=>{f=new Map,w=D.showData,tt()},"clear"),lt=i(({label:t,value:a})=>{f.has(t)||(f.set(t,a),C.debug(`added new section: ${t}, with value: ${a}`))},"addSection"),ct=i(()=>f,"getSections"),pt=i(t=>{w=t},"setShowData"),dt=i(()=>w,"getShowData"),F={getConfig:ot,clear:nt,setDiagramTitle:V,getDiagramTitle:K,setAccTitle:H,getAccTitle:q,setAccDescription:U,getAccDescription:B,addSection:lt,getSections:ct,setShowData:pt,getShowData:dt},gt=i((t,a)=>{N(t,a),a.setShowData(t.showData),t.sections.map(a.addSection)},"populateDb"),ut={parse:i(async t=>{const a=await rt("pie",t);C.debug(a),gt(a,F)},"parse")},mt=i(t=>`
.pieCircle{
stroke: ${t.pieStrokeColor};
stroke-width : ${t.pieStrokeWidth};

View File

@ -1 +1 @@
import{s as r,b as e,a,S as s}from"./chunk-AEK57VVT-kQQtlouG.js";import{_ as i}from"./mermaid-vendor-CY273lNM.js";import"./chunk-RZ5BOZE2-BJpHTKlK.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};
import{s as r,b as e,a,S as s}from"./chunk-AEK57VVT--Edz4Twz.js";import{_ as i}from"./mermaid-vendor-DGPC_TDM.js";import"./chunk-RZ5BOZE2-BhCqM4LN.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};

View File

@ -1,4 +1,4 @@
import{_ as s,c as xt,l as T,d as q,a2 as kt,a3 as _t,a4 as bt,a5 as vt,N as nt,D as wt,a6 as St,z as Et}from"./mermaid-vendor-CY273lNM.js";import"./feature-graph-C2oTJZJ8.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var X=function(){var n=s(function(f,i,a,d){for(a=a||{},d=f.length;d--;a[f[d]]=i);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],r=[1,11],h=[1,12],c=[1,13],g=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(i,a,d,u,y,o,S){var k=o.length-1;switch(y){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 15:u.addTask(o[k],0,""),this.$=o[k];break;case 16:u.addEvent(o[k].substr(2)),this.$=o[k];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(i,a){if(a.recoverable)this.trace(i);else{var d=new Error(i);throw d.hash=a,d}},"parseError"),parse:s(function(i){var a=this,d=[0],u=[],y=[null],o=[],S=this.table,k="",M=0,P=0,B=2,J=1,O=o.slice.call(arguments,1),_=Object.create(this.lexer),E={yy:{}};for(var v in this.yy)Object.prototype.hasOwnProperty.call(this.yy,v)&&(E.yy[v]=this.yy[v]);_.setInput(i,E.yy),E.yy.lexer=_,E.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var L=_.yylloc;o.push(L);var A=_.options&&_.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function R(I){d.length=d.length-2*I,y.length=y.length-I,o.length=o.length-I}s(R,"popStack");function z(){var I;return I=u.pop()||_.lex()||J,typeof I!="number"&&(I instanceof Array&&(u=I,I=u.pop()),I=a.symbols_[I]||I),I}s(z,"lex");for(var w,C,N,K,F={},j,$,et,G;;){if(C=d[d.length-1],this.defaultActions[C]?N=this.defaultActions[C]:((w===null||typeof w>"u")&&(w=z()),N=S[C]&&S[C][w]),typeof N>"u"||!N.length||!N[0]){var Q="";G=[];for(j in S[C])this.terminals_[j]&&j>B&&G.push("'"+this.terminals_[j]+"'");_.showPosition?Q="Parse error on line "+(M+1)+`:
import{_ as s,c as xt,l as T,d as q,a2 as kt,a3 as _t,a4 as bt,a5 as vt,N as nt,D as wt,a6 as St,z as Et}from"./mermaid-vendor-DGPC_TDM.js";import"./feature-graph-Chuw_ipx.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var X=function(){var n=s(function(f,i,a,d){for(a=a||{},d=f.length;d--;a[f[d]]=i);return a},"o"),t=[6,8,10,11,12,14,16,17,20,21],e=[1,9],l=[1,10],r=[1,11],h=[1,12],c=[1,13],g=[1,16],m=[1,17],p={trace:s(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,period_statement:18,event_statement:19,period:20,event:21,$accept:0,$end:1},terminals_:{2:"error",4:"timeline",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",20:"period",21:"event"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[18,1],[19,1]],performAction:s(function(i,a,d,u,y,o,S){var k=o.length-1;switch(y){case 1:return o[k-1];case 2:this.$=[];break;case 3:o[k-1].push(o[k]),this.$=o[k-1];break;case 4:case 5:this.$=o[k];break;case 6:case 7:this.$=[];break;case 8:u.getCommonDb().setDiagramTitle(o[k].substr(6)),this.$=o[k].substr(6);break;case 9:this.$=o[k].trim(),u.getCommonDb().setAccTitle(this.$);break;case 10:case 11:this.$=o[k].trim(),u.getCommonDb().setAccDescription(this.$);break;case 12:u.addSection(o[k].substr(8)),this.$=o[k].substr(8);break;case 15:u.addTask(o[k],0,""),this.$=o[k];break;case 16:u.addEvent(o[k].substr(2)),this.$=o[k];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},n(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,7],{1:[2,1]}),n(t,[2,3]),{9:18,11:e,12:l,14:r,16:h,17:c,18:14,19:15,20:g,21:m},n(t,[2,5]),n(t,[2,6]),n(t,[2,8]),{13:[1,19]},{15:[1,20]},n(t,[2,11]),n(t,[2,12]),n(t,[2,13]),n(t,[2,14]),n(t,[2,15]),n(t,[2,16]),n(t,[2,4]),n(t,[2,9]),n(t,[2,10])],defaultActions:{},parseError:s(function(i,a){if(a.recoverable)this.trace(i);else{var d=new Error(i);throw d.hash=a,d}},"parseError"),parse:s(function(i){var a=this,d=[0],u=[],y=[null],o=[],S=this.table,k="",M=0,P=0,B=2,J=1,O=o.slice.call(arguments,1),_=Object.create(this.lexer),E={yy:{}};for(var v in this.yy)Object.prototype.hasOwnProperty.call(this.yy,v)&&(E.yy[v]=this.yy[v]);_.setInput(i,E.yy),E.yy.lexer=_,E.yy.parser=this,typeof _.yylloc>"u"&&(_.yylloc={});var L=_.yylloc;o.push(L);var A=_.options&&_.options.ranges;typeof E.yy.parseError=="function"?this.parseError=E.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function R(I){d.length=d.length-2*I,y.length=y.length-I,o.length=o.length-I}s(R,"popStack");function z(){var I;return I=u.pop()||_.lex()||J,typeof I!="number"&&(I instanceof Array&&(u=I,I=u.pop()),I=a.symbols_[I]||I),I}s(z,"lex");for(var w,C,N,K,F={},j,$,et,G;;){if(C=d[d.length-1],this.defaultActions[C]?N=this.defaultActions[C]:((w===null||typeof w>"u")&&(w=z()),N=S[C]&&S[C][w]),typeof N>"u"||!N.length||!N[0]){var Q="";G=[];for(j in S[C])this.terminals_[j]&&j>B&&G.push("'"+this.terminals_[j]+"'");_.showPosition?Q="Parse error on line "+(M+1)+`:
`+_.showPosition()+`
Expecting `+G.join(", ")+", got '"+(this.terminals_[w]||w)+"'":Q="Parse error on line "+(M+1)+": Unexpected "+(w==J?"end of input":"'"+(this.terminals_[w]||w)+"'"),this.parseError(Q,{text:_.match,token:this.terminals_[w]||w,line:_.yylineno,loc:L,expected:G})}if(N[0]instanceof Array&&N.length>1)throw new Error("Parse Error: multiple actions possible at state: "+C+", token: "+w);switch(N[0]){case 1:d.push(w),y.push(_.yytext),o.push(_.yylloc),d.push(N[1]),w=null,P=_.yyleng,k=_.yytext,M=_.yylineno,L=_.yylloc;break;case 2:if($=this.productions_[N[1]][1],F.$=y[y.length-$],F._$={first_line:o[o.length-($||1)].first_line,last_line:o[o.length-1].last_line,first_column:o[o.length-($||1)].first_column,last_column:o[o.length-1].last_column},A&&(F._$.range=[o[o.length-($||1)].range[0],o[o.length-1].range[1]]),K=this.performAction.apply(F,[k,P,M,E.yy,N[1],y,o].concat(O)),typeof K<"u")return K;$&&(d=d.slice(0,-1*$*2),y=y.slice(0,-1*$),o=o.slice(0,-1*$)),d.push(this.productions_[N[1]][0]),y.push(F.$),o.push(F._$),et=S[d[d.length-2]][d[d.length-1]],d.push(et);break;case 3:return!0}}return!0},"parse")},x=function(){var f={EOF:1,parseError:s(function(a,d){if(this.yy.parser)this.yy.parser.parseError(a,d);else throw new Error(a)},"parseError"),setInput:s(function(i,a){return this.yy=a||this.yy||{},this._input=i,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:s(function(){var i=this._input[0];this.yytext+=i,this.yyleng++,this.offset++,this.match+=i,this.matched+=i;var a=i.match(/(?:\r\n?|\n).*/g);return a?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),i},"input"),unput:s(function(i){var a=i.length,d=i.split(/(?:\r\n?|\n)/g);this._input=i+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-a),this.offset-=a;var u=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),d.length-1&&(this.yylineno-=d.length-1);var y=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:d?(d.length===u.length?this.yylloc.first_column:0)+u[u.length-d.length].length-d[0].length:this.yylloc.first_column-a},this.options.ranges&&(this.yylloc.range=[y[0],y[0]+this.yyleng-a]),this.yyleng=this.yytext.length,this},"unput"),more:s(function(){return this._more=!0,this},"more"),reject:s(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).
`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:s(function(i){this.unput(this.match.slice(i))},"less"),pastInput:s(function(){var i=this.matched.substr(0,this.matched.length-this.match.length);return(i.length>20?"...":"")+i.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:s(function(){var i=this.match;return i.length<20&&(i+=this._input.substr(0,20-i.length)),(i.substr(0,20)+(i.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:s(function(){var i=this.pastInput(),a=new Array(i.length+1).join("-");return i+this.upcomingInput()+`

View File

@ -8,18 +8,18 @@
<link rel="icon" type="image/png" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-BghbDBWC.js"></script>
<script type="module" crossorigin src="/webui/assets/index-C3sDhLdo.js"></script>
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
<link rel="modulepreload" crossorigin href="/webui/assets/utils-vendor-BysuhMZA.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-C2oTJZJ8.js">
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-CY273lNM.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-Chuw_ipx.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-36AWafKt.js">
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-DGPC_TDM.js">
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-DmIvJdn7.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-CpuDBjCo.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-CHAt2SlA.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-Bsekt8JN.js">
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
<link rel="stylesheet" crossorigin href="/webui/assets/index-DwO2XWaU.css">
<link rel="stylesheet" crossorigin href="/webui/assets/index-CafJWW1u.css">
</head>
<body>
<div id="root"></div>

View File

@ -23,6 +23,11 @@ from .constants import (
DEFAULT_MAX_TOTAL_TOKENS,
DEFAULT_HISTORY_TURNS,
DEFAULT_ENABLE_RERANK,
DEFAULT_OLLAMA_MODEL_NAME,
DEFAULT_OLLAMA_MODEL_TAG,
DEFAULT_OLLAMA_MODEL_SIZE,
DEFAULT_OLLAMA_CREATED_AT,
DEFAULT_OLLAMA_DIGEST,
)
# use the .env that is inside the current folder
@ -31,6 +36,39 @@ from .constants import (
load_dotenv(dotenv_path=".env", override=False)
class OllamaServerInfos:
def __init__(self, name=None, tag=None):
self._lightrag_name = name or os.getenv(
"OLLAMA_EMULATING_MODEL_NAME", DEFAULT_OLLAMA_MODEL_NAME
)
self._lightrag_tag = tag or os.getenv(
"OLLAMA_EMULATING_MODEL_TAG", DEFAULT_OLLAMA_MODEL_TAG
)
self.LIGHTRAG_SIZE = DEFAULT_OLLAMA_MODEL_SIZE
self.LIGHTRAG_CREATED_AT = DEFAULT_OLLAMA_CREATED_AT
self.LIGHTRAG_DIGEST = DEFAULT_OLLAMA_DIGEST
@property
def LIGHTRAG_NAME(self):
return self._lightrag_name
@LIGHTRAG_NAME.setter
def LIGHTRAG_NAME(self, value):
self._lightrag_name = value
@property
def LIGHTRAG_TAG(self):
return self._lightrag_tag
@LIGHTRAG_TAG.setter
def LIGHTRAG_TAG(self, value):
self._lightrag_tag = value
@property
def LIGHTRAG_MODEL(self):
return f"{self._lightrag_name}:{self._lightrag_tag}"
class TextChunkSchema(TypedDict):
tokens: int
content: str
@ -100,6 +138,7 @@ class QueryParam:
Format: [{"role": "user/assistant", "content": "message"}].
"""
# Deprecated: history message have negtive effect on query performance
history_turns: int = int(os.getenv("HISTORY_TURNS", str(DEFAULT_HISTORY_TURNS)))
"""Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
@ -629,8 +668,6 @@ class DocStatus(str, Enum):
class DocProcessingStatus:
"""Document processing status data structure"""
content: str
"""Original content of the document"""
content_summary: str
"""First 100 chars of document content, used for preview"""
content_length: int
@ -643,11 +680,13 @@ class DocProcessingStatus:
"""ISO format timestamp when document was created"""
updated_at: str
"""ISO format timestamp when document was last updated"""
track_id: str | None = None
"""Tracking ID for monitoring progress"""
chunks_count: int | None = None
"""Number of chunks after splitting, used for processing"""
chunks_list: list[str] | None = field(default_factory=list)
"""List of chunk IDs associated with this document, used for deletion"""
error: str | None = None
error_msg: str | None = None
"""Error message if failed"""
metadata: dict[str, Any] = field(default_factory=dict)
"""Additional metadata"""
@ -667,6 +706,42 @@ class DocStatusStorage(BaseKVStorage, ABC):
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific status"""
@abstractmethod
async def get_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific track_id"""
@abstractmethod
async def get_docs_paginated(
self,
status_filter: DocStatus | None = None,
page: int = 1,
page_size: int = 50,
sort_field: str = "updated_at",
sort_direction: str = "desc",
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
"""Get documents with pagination support
Args:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
sort_direction: Sort direction ('asc' or 'desc')
Returns:
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
"""
@abstractmethod
async def get_all_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status for all documents
Returns:
Dictionary mapping status names to counts
"""
async def drop_cache_by_modes(self, modes: list[str] | None = None) -> bool:
"""Drop cache is not supported for Doc Status storage"""
return False

View File

@ -6,27 +6,59 @@ different parts of the LightRAG system. Centralizing these values ensures
consistency and makes maintenance easier.
"""
# Default values for environment variables
DEFAULT_MAX_GLEANING = 1
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE = 4
# Default values for server settings
DEFAULT_WOKERS = 2
DEFAULT_TIMEOUT = 150
DEFAULT_MAX_GRAPH_NODES = 1000
# Default values for extraction settings
DEFAULT_SUMMARY_LANGUAGE = "English" # Default language for summaries
DEFAULT_FORCE_LLM_SUMMARY_ON_MERGE = 4
DEFAULT_MAX_GLEANING = 1
DEFAULT_SUMMARY_MAX_TOKENS = 10000 # Default maximum token size
# Separator for graph fields
GRAPH_FIELD_SEP = "<SEP>"
# Query and retrieval configuration defaults
DEFAULT_TOP_K = 40
DEFAULT_CHUNK_TOP_K = 10
DEFAULT_MAX_ENTITY_TOKENS = 10000
DEFAULT_MAX_RELATION_TOKENS = 10000
DEFAULT_MAX_TOTAL_TOKENS = 32000
DEFAULT_HISTORY_TURNS = 0
DEFAULT_ENABLE_RERANK = True
DEFAULT_MAX_TOTAL_TOKENS = 30000
DEFAULT_COSINE_THRESHOLD = 0.2
DEFAULT_RELATED_CHUNK_NUMBER = 10
DEFAULT_RELATED_CHUNK_NUMBER = 5
# Deprated: history message have negtive effect on query performance
DEFAULT_HISTORY_TURNS = 0
# Separator for graph fields
GRAPH_FIELD_SEP = "<SEP>"
# Rerank configuration defaults
DEFAULT_ENABLE_RERANK = True
DEFAULT_MIN_RERANK_SCORE = 0.0
# File path configuration for vector and graph database(Should not be changed, used in Milvus Schema)
DEFAULT_MAX_FILE_PATH_LENGTH = 4090
# Default temperature for LLM
DEFAULT_TEMPERATURE = 1.0
# Async configuration defaults
DEFAULT_MAX_ASYNC = 4 # Default maximum async operations
DEFAULT_MAX_PARALLEL_INSERT = 2 # Default maximum parallel insert operations
# Embedding configuration defaults
DEFAULT_EMBEDDING_FUNC_MAX_ASYNC = 8 # Default max async for embedding functions
DEFAULT_EMBEDDING_BATCH_NUM = 10 # Default batch size for embedding computations
# Ollama Server Timetout in seconds
DEFAULT_TIMEOUT = 150
# Logging configuration defaults
DEFAULT_LOG_MAX_BYTES = 10485760 # Default 10MB
DEFAULT_LOG_BACKUP_COUNT = 5 # Default 5 backups
DEFAULT_LOG_FILENAME = "lightrag.log" # Default log filename
# Ollama server configuration defaults
DEFAULT_OLLAMA_MODEL_NAME = "lightrag"
DEFAULT_OLLAMA_MODEL_TAG = "latest"
DEFAULT_OLLAMA_MODEL_SIZE = 7365960935
DEFAULT_OLLAMA_CREATED_AT = "2024-01-15T00:00:00Z"
DEFAULT_OLLAMA_DIGEST = "sha256:lightrag"

View File

@ -45,21 +45,22 @@ class JsonDocStatusStorage(DocStatusStorage):
self._data = None
self._storage_lock = None
self.storage_updated = None
self.final_namespace = f"{self.workspace}_{self.namespace}"
async def initialize(self):
"""Initialize storage data"""
self._storage_lock = get_storage_lock()
self.storage_updated = await get_update_flag(self.namespace)
self.storage_updated = await get_update_flag(self.final_namespace)
async with get_data_init_lock():
# check need_init must before get_namespace_data
need_init = await try_initialize_namespace(self.namespace)
self._data = await get_namespace_data(self.namespace)
need_init = await try_initialize_namespace(self.final_namespace)
self._data = await get_namespace_data(self.final_namespace)
if need_init:
loaded_data = load_json(self._file_name) or {}
async with self._storage_lock:
self._data.update(loaded_data)
logger.info(
f"Process {os.getpid()} doc status load {self.namespace} with {len(loaded_data)} records"
f"Process {os.getpid()} doc status load {self.final_namespace} with {len(loaded_data)} records"
)
async def filter_keys(self, keys: set[str]) -> set[str]:
@ -95,12 +96,43 @@ class JsonDocStatusStorage(DocStatusStorage):
try:
# Make a copy of the data to avoid modifying the original
data = v.copy()
# If content is missing, use content_summary as content
if "content" not in data and "content_summary" in data:
data["content"] = data["content_summary"]
# Remove deprecated content field if it exists
data.pop("content", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
result[k] = DocProcessingStatus(**data)
except KeyError as e:
logger.error(f"Missing required field for document {k}: {e}")
continue
return result
async def get_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific track_id"""
result = {}
async with self._storage_lock:
for k, v in self._data.items():
if v.get("track_id") == track_id:
try:
# Make a copy of the data to avoid modifying the original
data = v.copy()
# Remove deprecated content field if it exists
data.pop("content", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
result[k] = DocProcessingStatus(**data)
except KeyError as e:
logger.error(f"Missing required field for document {k}: {e}")
@ -114,10 +146,10 @@ class JsonDocStatusStorage(DocStatusStorage):
dict(self._data) if hasattr(self._data, "_getvalue") else self._data
)
logger.debug(
f"Process {os.getpid()} doc status writting {len(data_dict)} records to {self.namespace}"
f"Process {os.getpid()} doc status writting {len(data_dict)} records to {self.final_namespace}"
)
write_json(data_dict, self._file_name)
await clear_all_update_flags(self.namespace)
await clear_all_update_flags(self.final_namespace)
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
"""
@ -127,14 +159,14 @@ class JsonDocStatusStorage(DocStatusStorage):
"""
if not data:
return
logger.debug(f"Inserting {len(data)} records to {self.namespace}")
logger.debug(f"Inserting {len(data)} records to {self.final_namespace}")
async with self._storage_lock:
# Ensure chunks_list field exists for new documents
for doc_id, doc_data in data.items():
if "chunks_list" not in doc_data:
doc_data["chunks_list"] = []
self._data.update(data)
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
await self.index_done_callback()
@ -142,6 +174,111 @@ class JsonDocStatusStorage(DocStatusStorage):
async with self._storage_lock:
return self._data.get(id)
async def get_docs_paginated(
self,
status_filter: DocStatus | None = None,
page: int = 1,
page_size: int = 50,
sort_field: str = "updated_at",
sort_direction: str = "desc",
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
"""Get documents with pagination support
Args:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
sort_direction: Sort direction ('asc' or 'desc')
Returns:
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
"""
# Validate parameters
if page < 1:
page = 1
if page_size < 10:
page_size = 10
elif page_size > 200:
page_size = 200
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
sort_field = "updated_at"
if sort_direction.lower() not in ["asc", "desc"]:
sort_direction = "desc"
# For JSON storage, we load all data and sort/filter in memory
all_docs = []
async with self._storage_lock:
for doc_id, doc_data in self._data.items():
# Apply status filter
if (
status_filter is not None
and doc_data.get("status") != status_filter.value
):
continue
try:
# Prepare document data
data = doc_data.copy()
data.pop("content", None)
if "file_path" not in data:
data["file_path"] = "no-file-path"
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
doc_status = DocProcessingStatus(**data)
# Add sort key for sorting
if sort_field == "id":
doc_status._sort_key = doc_id
else:
doc_status._sort_key = getattr(doc_status, sort_field, "")
all_docs.append((doc_id, doc_status))
except KeyError as e:
logger.error(f"Error processing document {doc_id}: {e}")
continue
# Sort documents
reverse_sort = sort_direction.lower() == "desc"
all_docs.sort(
key=lambda x: getattr(x[1], "_sort_key", ""), reverse=reverse_sort
)
# Remove sort key from documents
for doc_id, doc in all_docs:
if hasattr(doc, "_sort_key"):
delattr(doc, "_sort_key")
total_count = len(all_docs)
# Apply pagination
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_docs = all_docs[start_idx:end_idx]
return paginated_docs, total_count
async def get_all_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status for all documents
Returns:
Dictionary mapping status names to counts, including 'all' field
"""
counts = await self.get_status_counts()
# Add 'all' field with total count
total_count = sum(counts.values())
counts["all"] = total_count
return counts
async def delete(self, doc_ids: list[str]) -> None:
"""Delete specific records from storage by their IDs
@ -163,7 +300,7 @@ class JsonDocStatusStorage(DocStatusStorage):
any_deleted = True
if any_deleted:
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
async def drop(self) -> dict[str, str]:
"""Drop all document status data from storage and clean up resources
@ -181,11 +318,11 @@ class JsonDocStatusStorage(DocStatusStorage):
try:
async with self._storage_lock:
self._data.clear()
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
await self.index_done_callback()
logger.info(f"Process {os.getpid()} drop {self.namespace}")
logger.info(f"Process {os.getpid()} drop {self.final_namespace}")
return {"status": "success", "message": "data dropped"}
except Exception as e:
logger.error(f"Error dropping {self.namespace}: {e}")
logger.error(f"Error dropping {self.final_namespace}: {e}")
return {"status": "error", "message": str(e)}

View File

@ -41,15 +41,16 @@ class JsonKVStorage(BaseKVStorage):
self._data = None
self._storage_lock = None
self.storage_updated = None
self.final_namespace = f"{self.workspace}_{self.namespace}"
async def initialize(self):
"""Initialize storage data"""
self._storage_lock = get_storage_lock()
self.storage_updated = await get_update_flag(self.namespace)
self.storage_updated = await get_update_flag(self.final_namespace)
async with get_data_init_lock():
# check need_init must before get_namespace_data
need_init = await try_initialize_namespace(self.namespace)
self._data = await get_namespace_data(self.namespace)
need_init = await try_initialize_namespace(self.final_namespace)
self._data = await get_namespace_data(self.final_namespace)
if need_init:
loaded_data = load_json(self._file_name) or {}
async with self._storage_lock:
@ -63,7 +64,7 @@ class JsonKVStorage(BaseKVStorage):
data_count = len(loaded_data)
logger.info(
f"Process {os.getpid()} KV load {self.namespace} with {data_count} records"
f"Process {os.getpid()} KV load {self.final_namespace} with {data_count} records"
)
async def index_done_callback(self) -> None:
@ -77,10 +78,10 @@ class JsonKVStorage(BaseKVStorage):
data_count = len(data_dict)
logger.debug(
f"Process {os.getpid()} KV writting {data_count} records to {self.namespace}"
f"Process {os.getpid()} KV writting {data_count} records to {self.final_namespace}"
)
write_json(data_dict, self._file_name)
await clear_all_update_flags(self.namespace)
await clear_all_update_flags(self.final_namespace)
async def get_all(self) -> dict[str, Any]:
"""Get all data from storage
@ -150,7 +151,7 @@ class JsonKVStorage(BaseKVStorage):
current_time = int(time.time()) # Get current Unix timestamp
logger.debug(f"Inserting {len(data)} records to {self.namespace}")
logger.debug(f"Inserting {len(data)} records to {self.final_namespace}")
async with self._storage_lock:
# Add timestamps to data based on whether key exists
for k, v in data.items():
@ -169,7 +170,7 @@ class JsonKVStorage(BaseKVStorage):
v["_id"] = k
self._data.update(data)
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
async def delete(self, ids: list[str]) -> None:
"""Delete specific records from storage by their IDs
@ -192,7 +193,7 @@ class JsonKVStorage(BaseKVStorage):
any_deleted = True
if any_deleted:
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
async def drop_cache_by_modes(self, modes: list[str] | None = None) -> bool:
"""Delete specific records from storage by cache mode
@ -227,7 +228,7 @@ class JsonKVStorage(BaseKVStorage):
self._data.pop(key, None)
if keys_to_delete:
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
logger.info(
f"Dropped {len(keys_to_delete)} cache entries for modes: {modes}"
)
@ -276,7 +277,7 @@ class JsonKVStorage(BaseKVStorage):
# del self._data[mode_key]
# # Set update flags to notify persistence is needed
# await set_all_update_flags(self.namespace)
# await set_all_update_flags(self.final_namespace)
# logger.info(f"Cleared cache for {len(chunk_ids)} chunk IDs")
# return True
@ -301,13 +302,13 @@ class JsonKVStorage(BaseKVStorage):
try:
async with self._storage_lock:
self._data.clear()
await set_all_update_flags(self.namespace)
await set_all_update_flags(self.final_namespace)
await self.index_done_callback()
logger.info(f"Process {os.getpid()} drop {self.namespace}")
logger.info(f"Process {os.getpid()} drop {self.final_namespace}")
return {"status": "success", "message": "data dropped"}
except Exception as e:
logger.error(f"Error dropping {self.namespace}: {e}")
logger.error(f"Error dropping {self.final_namespace}: {e}")
return {"status": "error", "message": str(e)}
async def _migrate_legacy_cache_structure(self, data: dict) -> dict:

View File

@ -5,6 +5,7 @@ from dataclasses import dataclass
import numpy as np
from lightrag.utils import logger, compute_mdhash_id
from ..base import BaseVectorStorage
from ..constants import DEFAULT_MAX_FILE_PATH_LENGTH
import pipmaster as pm
if not pm.is_installed("pymilvus"):
@ -47,7 +48,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
FieldSchema(
name="file_path",
dtype=DataType.VARCHAR,
max_length=1024,
max_length=DEFAULT_MAX_FILE_PATH_LENGTH,
nullable=True,
),
]
@ -64,7 +65,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
FieldSchema(
name="file_path",
dtype=DataType.VARCHAR,
max_length=1024,
max_length=DEFAULT_MAX_FILE_PATH_LENGTH,
nullable=True,
),
]
@ -536,7 +537,7 @@ class MilvusVectorDBStorage(BaseVectorStorage):
# Load the collection if it's not already loaded
# In Milvus, collections need to be loaded before they can be searched
self._client.load_collection(self.namespace)
logger.debug(f"Collection {self.namespace} loaded successfully")
# logger.debug(f"Collection {self.namespace} loaded successfully")
except Exception as e:
logger.error(f"Failed to load collection {self.namespace}: {e}")

View File

@ -321,6 +321,13 @@ class MongoDocStatusStorage(DocStatusStorage):
if self.db is None:
self.db = await ClientManager.get_client()
self._data = await get_or_create_collection(self.db, self._collection_name)
# Create track_id index for better query performance
await self.create_track_id_index_if_not_exists()
# Create pagination indexes for better query performance
await self.create_pagination_indexes_if_not_exists()
logger.debug(f"Use MongoDB as DocStatus {self._collection_name}")
async def finalize(self):
@ -359,7 +366,7 @@ class MongoDocStatusStorage(DocStatusStorage):
async def get_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status"""
pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}]
cursor = self._data.aggregate(pipeline, allowDiskUse=True)
cursor = await self._data.aggregate(pipeline, allowDiskUse=True)
result = await cursor.to_list()
counts = {}
for doc in result:
@ -372,20 +379,57 @@ class MongoDocStatusStorage(DocStatusStorage):
"""Get all documents with a specific status"""
cursor = self._data.find({"status": status.value})
result = await cursor.to_list()
return {
doc["_id"]: DocProcessingStatus(
content=doc["content"],
content_summary=doc.get("content_summary"),
content_length=doc["content_length"],
status=doc["status"],
created_at=doc.get("created_at"),
updated_at=doc.get("updated_at"),
chunks_count=doc.get("chunks_count", -1),
file_path=doc.get("file_path", doc["_id"]),
chunks_list=doc.get("chunks_list", []),
)
for doc in result
}
processed_result = {}
for doc in result:
try:
# Make a copy of the data to avoid modifying the original
data = doc.copy()
# Remove deprecated content field if it exists
data.pop("content", None)
# Remove MongoDB _id field if it exists
data.pop("_id", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
processed_result[doc["_id"]] = DocProcessingStatus(**data)
except KeyError as e:
logger.error(f"Missing required field for document {doc['_id']}: {e}")
continue
return processed_result
async def get_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific track_id"""
cursor = self._data.find({"track_id": track_id})
result = await cursor.to_list()
processed_result = {}
for doc in result:
try:
# Make a copy of the data to avoid modifying the original
data = doc.copy()
# Remove deprecated content field if it exists
data.pop("content", None)
# Remove MongoDB _id field if it exists
data.pop("_id", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
processed_result[doc["_id"]] = DocProcessingStatus(**data)
except KeyError as e:
logger.error(f"Missing required field for document {doc['_id']}: {e}")
continue
return processed_result
async def index_done_callback(self) -> None:
# Mongo handles persistence automatically
@ -415,6 +459,181 @@ class MongoDocStatusStorage(DocStatusStorage):
async def delete(self, ids: list[str]) -> None:
await self._data.delete_many({"_id": {"$in": ids}})
async def create_track_id_index_if_not_exists(self):
"""Create track_id index for better query performance"""
try:
# Check if index already exists
indexes_cursor = await self._data.list_indexes()
existing_indexes = await indexes_cursor.to_list(length=None)
track_id_index_exists = any(
"track_id" in idx.get("key", {}) for idx in existing_indexes
)
if not track_id_index_exists:
await self._data.create_index("track_id")
logger.info(
f"Created track_id index for collection {self._collection_name}"
)
else:
logger.debug(
f"track_id index already exists for collection {self._collection_name}"
)
except PyMongoError as e:
logger.error(
f"Error creating track_id index for {self._collection_name}: {e}"
)
async def create_pagination_indexes_if_not_exists(self):
"""Create indexes to optimize pagination queries"""
try:
indexes_cursor = await self._data.list_indexes()
existing_indexes = await indexes_cursor.to_list(length=None)
# Define indexes needed for pagination
pagination_indexes = [
{
"name": "status_updated_at",
"keys": [("status", 1), ("updated_at", -1)],
},
{
"name": "status_created_at",
"keys": [("status", 1), ("created_at", -1)],
},
{"name": "updated_at", "keys": [("updated_at", -1)]},
{"name": "created_at", "keys": [("created_at", -1)]},
{"name": "id", "keys": [("_id", 1)]},
{"name": "file_path", "keys": [("file_path", 1)]},
]
# Check which indexes already exist
existing_index_names = {idx.get("name", "") for idx in existing_indexes}
for index_info in pagination_indexes:
index_name = index_info["name"]
if index_name not in existing_index_names:
await self._data.create_index(index_info["keys"], name=index_name)
logger.info(
f"Created pagination index '{index_name}' for collection {self._collection_name}"
)
else:
logger.debug(
f"Pagination index '{index_name}' already exists for collection {self._collection_name}"
)
except PyMongoError as e:
logger.error(
f"Error creating pagination indexes for {self._collection_name}: {e}"
)
async def get_docs_paginated(
self,
status_filter: DocStatus | None = None,
page: int = 1,
page_size: int = 50,
sort_field: str = "updated_at",
sort_direction: str = "desc",
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
"""Get documents with pagination support
Args:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', '_id')
sort_direction: Sort direction ('asc' or 'desc')
Returns:
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
"""
# Validate parameters
if page < 1:
page = 1
if page_size < 10:
page_size = 10
elif page_size > 200:
page_size = 200
if sort_field not in ["created_at", "updated_at", "_id", "file_path"]:
sort_field = "updated_at"
if sort_direction.lower() not in ["asc", "desc"]:
sort_direction = "desc"
# Build query filter
query_filter = {}
if status_filter is not None:
query_filter["status"] = status_filter.value
# Get total count
total_count = await self._data.count_documents(query_filter)
# Calculate skip value
skip = (page - 1) * page_size
# Build sort criteria
sort_direction_value = 1 if sort_direction.lower() == "asc" else -1
sort_criteria = [(sort_field, sort_direction_value)]
# Query for paginated data
cursor = (
self._data.find(query_filter)
.sort(sort_criteria)
.skip(skip)
.limit(page_size)
)
result = await cursor.to_list(length=page_size)
# Convert to (doc_id, DocProcessingStatus) tuples
documents = []
for doc in result:
try:
doc_id = doc["_id"]
# Make a copy of the data to avoid modifying the original
data = doc.copy()
# Remove deprecated content field if it exists
data.pop("content", None)
# Remove MongoDB _id field if it exists
data.pop("_id", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
doc_status = DocProcessingStatus(**data)
documents.append((doc_id, doc_status))
except KeyError as e:
logger.error(f"Missing required field for document {doc['_id']}: {e}")
continue
return documents, total_count
async def get_all_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status for all documents
Returns:
Dictionary mapping status names to counts, including 'all' field
"""
pipeline = [{"$group": {"_id": "$status", "count": {"$sum": 1}}}]
cursor = await self._data.aggregate(pipeline, allowDiskUse=True)
result = await cursor.to_list()
counts = {}
total_count = 0
for doc in result:
counts[doc["_id"]] = doc["count"]
total_count += doc["count"]
# Add 'all' field with total count
counts["all"] = total_count
return counts
@final
@dataclass

View File

@ -9,12 +9,8 @@ from lightrag.utils import (
logger,
compute_mdhash_id,
)
import pipmaster as pm
from lightrag.base import BaseVectorStorage
if not pm.is_installed("nano-vectordb"):
pm.install("nano-vectordb")
from nano_vectordb import NanoVectorDB
from .shared_storage import (
get_storage_lock,

View File

@ -6,12 +6,6 @@ from lightrag.types import KnowledgeGraph, KnowledgeGraphNode, KnowledgeGraphEdg
from lightrag.utils import logger
from lightrag.base import BaseGraphStorage
from lightrag.constants import GRAPH_FIELD_SEP
import pipmaster as pm
if not pm.is_installed("networkx"):
pm.install("networkx")
import networkx as nx
from .shared_storage import (
get_storage_lock,

View File

@ -80,7 +80,7 @@ class PostgreSQLDB:
if ssl_mode in ["disable", "allow", "prefer", "require"]:
if ssl_mode == "disable":
return None
elif ssl_mode in ["require", "prefer"]:
elif ssl_mode in ["require", "prefer", "allow"]:
# Return None for simple SSL requirement, handled in initdb
return None
@ -567,6 +567,119 @@ class PostgreSQLDB:
f"Failed to add llm_cache_list column to LIGHTRAG_DOC_CHUNKS: {e}"
)
async def _migrate_doc_status_add_track_id(self):
"""Add track_id column to LIGHTRAG_DOC_STATUS table if it doesn't exist and create index"""
try:
# Check if track_id column exists
check_column_sql = """
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'lightrag_doc_status'
AND column_name = 'track_id'
"""
column_info = await self.query(check_column_sql)
if not column_info:
logger.info("Adding track_id column to LIGHTRAG_DOC_STATUS table")
add_column_sql = """
ALTER TABLE LIGHTRAG_DOC_STATUS
ADD COLUMN track_id VARCHAR(255) NULL
"""
await self.execute(add_column_sql)
logger.info(
"Successfully added track_id column to LIGHTRAG_DOC_STATUS table"
)
else:
logger.info(
"track_id column already exists in LIGHTRAG_DOC_STATUS table"
)
# Check if track_id index exists
check_index_sql = """
SELECT indexname
FROM pg_indexes
WHERE tablename = 'lightrag_doc_status'
AND indexname = 'idx_lightrag_doc_status_track_id'
"""
index_info = await self.query(check_index_sql)
if not index_info:
logger.info(
"Creating index on track_id column for LIGHTRAG_DOC_STATUS table"
)
create_index_sql = """
CREATE INDEX idx_lightrag_doc_status_track_id ON LIGHTRAG_DOC_STATUS (track_id)
"""
await self.execute(create_index_sql)
logger.info(
"Successfully created index on track_id column for LIGHTRAG_DOC_STATUS table"
)
else:
logger.info(
"Index on track_id column already exists for LIGHTRAG_DOC_STATUS table"
)
except Exception as e:
logger.warning(
f"Failed to add track_id column or index to LIGHTRAG_DOC_STATUS: {e}"
)
async def _migrate_doc_status_add_metadata_error_msg(self):
"""Add metadata and error_msg columns to LIGHTRAG_DOC_STATUS table if they don't exist"""
try:
# Check if metadata column exists
check_metadata_sql = """
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'lightrag_doc_status'
AND column_name = 'metadata'
"""
metadata_info = await self.query(check_metadata_sql)
if not metadata_info:
logger.info("Adding metadata column to LIGHTRAG_DOC_STATUS table")
add_metadata_sql = """
ALTER TABLE LIGHTRAG_DOC_STATUS
ADD COLUMN metadata JSONB NULL DEFAULT '{}'::jsonb
"""
await self.execute(add_metadata_sql)
logger.info(
"Successfully added metadata column to LIGHTRAG_DOC_STATUS table"
)
else:
logger.info(
"metadata column already exists in LIGHTRAG_DOC_STATUS table"
)
# Check if error_msg column exists
check_error_msg_sql = """
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'lightrag_doc_status'
AND column_name = 'error_msg'
"""
error_msg_info = await self.query(check_error_msg_sql)
if not error_msg_info:
logger.info("Adding error_msg column to LIGHTRAG_DOC_STATUS table")
add_error_msg_sql = """
ALTER TABLE LIGHTRAG_DOC_STATUS
ADD COLUMN error_msg TEXT NULL
"""
await self.execute(add_error_msg_sql)
logger.info(
"Successfully added error_msg column to LIGHTRAG_DOC_STATUS table"
)
else:
logger.info(
"error_msg column already exists in LIGHTRAG_DOC_STATUS table"
)
except Exception as e:
logger.warning(
f"Failed to add metadata/error_msg columns to LIGHTRAG_DOC_STATUS: {e}"
)
async def _migrate_field_lengths(self):
"""Migrate database field lengths: entity_name, source_id, target_id, and file_path"""
# Define the field changes needed
@ -785,6 +898,85 @@ class PostgreSQLDB:
except Exception as e:
logger.error(f"PostgreSQL, Failed to migrate field lengths: {e}")
# Migrate doc status to add track_id field if needed
try:
await self._migrate_doc_status_add_track_id()
except Exception as e:
logger.error(
f"PostgreSQL, Failed to migrate doc status track_id field: {e}"
)
# Migrate doc status to add metadata and error_msg fields if needed
try:
await self._migrate_doc_status_add_metadata_error_msg()
except Exception as e:
logger.error(
f"PostgreSQL, Failed to migrate doc status metadata/error_msg fields: {e}"
)
# Create pagination optimization indexes for LIGHTRAG_DOC_STATUS
try:
await self._create_pagination_indexes()
except Exception as e:
logger.error(f"PostgreSQL, Failed to create pagination indexes: {e}")
async def _create_pagination_indexes(self):
"""Create indexes to optimize pagination queries for LIGHTRAG_DOC_STATUS"""
indexes = [
{
"name": "idx_lightrag_doc_status_workspace_status_updated_at",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_status_updated_at ON LIGHTRAG_DOC_STATUS (workspace, status, updated_at DESC)",
"description": "Composite index for workspace + status + updated_at pagination",
},
{
"name": "idx_lightrag_doc_status_workspace_status_created_at",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_status_created_at ON LIGHTRAG_DOC_STATUS (workspace, status, created_at DESC)",
"description": "Composite index for workspace + status + created_at pagination",
},
{
"name": "idx_lightrag_doc_status_workspace_updated_at",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_updated_at ON LIGHTRAG_DOC_STATUS (workspace, updated_at DESC)",
"description": "Index for workspace + updated_at pagination (all statuses)",
},
{
"name": "idx_lightrag_doc_status_workspace_created_at",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_created_at ON LIGHTRAG_DOC_STATUS (workspace, created_at DESC)",
"description": "Index for workspace + created_at pagination (all statuses)",
},
{
"name": "idx_lightrag_doc_status_workspace_id",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_id ON LIGHTRAG_DOC_STATUS (workspace, id)",
"description": "Index for workspace + id sorting",
},
{
"name": "idx_lightrag_doc_status_workspace_file_path",
"sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_lightrag_doc_status_workspace_file_path ON LIGHTRAG_DOC_STATUS (workspace, file_path)",
"description": "Index for workspace + file_path sorting",
},
]
for index in indexes:
try:
# Check if index already exists
check_sql = """
SELECT indexname
FROM pg_indexes
WHERE tablename = 'lightrag_doc_status'
AND indexname = $1
"""
existing = await self.query(check_sql, {"indexname": index["name"]})
if not existing:
logger.info(f"Creating pagination index: {index['description']}")
await self.execute(index["sql"])
logger.info(f"Successfully created index: {index['name']}")
else:
logger.debug(f"Index already exists: {index['name']}")
except Exception as e:
logger.warning(f"Failed to create index {index['name']}: {e}")
async def query(
self,
sql: str,
@ -1668,12 +1860,19 @@ class PGDocStatusStorage(DocStatusStorage):
except json.JSONDecodeError:
chunks_list = []
# Parse metadata JSON string back to dict
metadata = result[0].get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
# Convert datetime objects to ISO format strings with timezone info
created_at = self._format_datetime_with_timezone(result[0]["created_at"])
updated_at = self._format_datetime_with_timezone(result[0]["updated_at"])
return dict(
content=result[0]["content"],
content_length=result[0]["content_length"],
content_summary=result[0]["content_summary"],
status=result[0]["status"],
@ -1682,6 +1881,9 @@ class PGDocStatusStorage(DocStatusStorage):
updated_at=updated_at,
file_path=result[0]["file_path"],
chunks_list=chunks_list,
metadata=metadata,
error_msg=result[0].get("error_msg"),
track_id=result[0].get("track_id"),
)
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
@ -1707,13 +1909,20 @@ class PGDocStatusStorage(DocStatusStorage):
except json.JSONDecodeError:
chunks_list = []
# Parse metadata JSON string back to dict
metadata = row.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
# Convert datetime objects to ISO format strings with timezone info
created_at = self._format_datetime_with_timezone(row["created_at"])
updated_at = self._format_datetime_with_timezone(row["updated_at"])
processed_results.append(
{
"content": row["content"],
"content_length": row["content_length"],
"content_summary": row["content_summary"],
"status": row["status"],
@ -1722,6 +1931,9 @@ class PGDocStatusStorage(DocStatusStorage):
"updated_at": updated_at,
"file_path": row["file_path"],
"chunks_list": chunks_list,
"metadata": metadata,
"error_msg": row.get("error_msg"),
"track_id": row.get("track_id"),
}
)
@ -1757,12 +1969,189 @@ class PGDocStatusStorage(DocStatusStorage):
except json.JSONDecodeError:
chunks_list = []
# Parse metadata JSON string back to dict
metadata = element.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
# Ensure metadata is a dict
if not isinstance(metadata, dict):
metadata = {}
# Safe handling for file_path
file_path = element.get("file_path")
if file_path is None:
file_path = "no-file-path"
# Convert datetime objects to ISO format strings with timezone info
created_at = self._format_datetime_with_timezone(element["created_at"])
updated_at = self._format_datetime_with_timezone(element["updated_at"])
docs_by_status[element["id"]] = DocProcessingStatus(
content=element["content"],
content_summary=element["content_summary"],
content_length=element["content_length"],
status=element["status"],
created_at=created_at,
updated_at=updated_at,
chunks_count=element["chunks_count"],
file_path=file_path,
chunks_list=chunks_list,
metadata=metadata,
error_msg=element.get("error_msg"),
track_id=element.get("track_id"),
)
return docs_by_status
async def get_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific track_id"""
sql = "select * from LIGHTRAG_DOC_STATUS where workspace=$1 and track_id=$2"
params = {"workspace": self.db.workspace, "track_id": track_id}
result = await self.db.query(sql, params, True)
docs_by_track_id = {}
for element in result:
# Parse chunks_list JSON string back to list
chunks_list = element.get("chunks_list", [])
if isinstance(chunks_list, str):
try:
chunks_list = json.loads(chunks_list)
except json.JSONDecodeError:
chunks_list = []
# Parse metadata JSON string back to dict
metadata = element.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
# Ensure metadata is a dict
if not isinstance(metadata, dict):
metadata = {}
# Safe handling for file_path
file_path = element.get("file_path")
if file_path is None:
file_path = "no-file-path"
# Convert datetime objects to ISO format strings with timezone info
created_at = self._format_datetime_with_timezone(element["created_at"])
updated_at = self._format_datetime_with_timezone(element["updated_at"])
docs_by_track_id[element["id"]] = DocProcessingStatus(
content_summary=element["content_summary"],
content_length=element["content_length"],
status=element["status"],
created_at=created_at,
updated_at=updated_at,
chunks_count=element["chunks_count"],
file_path=file_path,
chunks_list=chunks_list,
track_id=element.get("track_id"),
metadata=metadata,
error_msg=element.get("error_msg"),
)
return docs_by_track_id
async def get_docs_paginated(
self,
status_filter: DocStatus | None = None,
page: int = 1,
page_size: int = 50,
sort_field: str = "updated_at",
sort_direction: str = "desc",
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
"""Get documents with pagination support
Args:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
sort_direction: Sort direction ('asc' or 'desc')
Returns:
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
"""
# Validate parameters
if page < 1:
page = 1
if page_size < 10:
page_size = 10
elif page_size > 200:
page_size = 200
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
sort_field = "updated_at"
if sort_direction.lower() not in ["asc", "desc"]:
sort_direction = "desc"
# Calculate offset
offset = (page - 1) * page_size
# Build WHERE clause
where_clause = "WHERE workspace=$1"
params = {"workspace": self.db.workspace}
param_count = 1
if status_filter is not None:
param_count += 1
where_clause += f" AND status=${param_count}"
params["status"] = status_filter.value
# Build ORDER BY clause
order_clause = f"ORDER BY {sort_field} {sort_direction.upper()}"
# Query for total count
count_sql = f"SELECT COUNT(*) as total FROM LIGHTRAG_DOC_STATUS {where_clause}"
count_result = await self.db.query(count_sql, params)
total_count = count_result["total"] if count_result else 0
# Query for paginated data
data_sql = f"""
SELECT * FROM LIGHTRAG_DOC_STATUS
{where_clause}
{order_clause}
LIMIT ${param_count + 1} OFFSET ${param_count + 2}
"""
params["limit"] = page_size
params["offset"] = offset
result = await self.db.query(data_sql, params, True)
# Convert to (doc_id, DocProcessingStatus) tuples
documents = []
for element in result:
doc_id = element["id"]
# Parse chunks_list JSON string back to list
chunks_list = element.get("chunks_list", [])
if isinstance(chunks_list, str):
try:
chunks_list = json.loads(chunks_list)
except json.JSONDecodeError:
chunks_list = []
# Parse metadata JSON string back to dict
metadata = element.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
# Convert datetime objects to ISO format strings with timezone info
created_at = self._format_datetime_with_timezone(element["created_at"])
updated_at = self._format_datetime_with_timezone(element["updated_at"])
doc_status = DocProcessingStatus(
content_summary=element["content_summary"],
content_length=element["content_length"],
status=element["status"],
@ -1771,9 +2160,39 @@ class PGDocStatusStorage(DocStatusStorage):
chunks_count=element["chunks_count"],
file_path=element["file_path"],
chunks_list=chunks_list,
track_id=element.get("track_id"),
metadata=metadata,
error_msg=element.get("error_msg"),
)
documents.append((doc_id, doc_status))
return docs_by_status
return documents, total_count
async def get_all_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status for all documents
Returns:
Dictionary mapping status names to counts, including 'all' field
"""
sql = """
SELECT status, COUNT(*) as count
FROM LIGHTRAG_DOC_STATUS
WHERE workspace=$1
GROUP BY status
"""
params = {"workspace": self.db.workspace}
result = await self.db.query(sql, params, True)
counts = {}
total_count = 0
for row in result:
counts[row["status"]] = row["count"]
total_count += row["count"]
# Add 'all' field with total count
counts["all"] = total_count
return counts
async def index_done_callback(self) -> None:
# PG handles persistence automatically
@ -1843,18 +2262,20 @@ class PGDocStatusStorage(DocStatusStorage):
logger.warning(f"Unable to parse datetime string: {dt_str}")
return None
# Modified SQL to include created_at, updated_at, and chunks_list in both INSERT and UPDATE operations
# Modified SQL to include created_at, updated_at, chunks_list, track_id, metadata, and error_msg in both INSERT and UPDATE operations
# All fields are updated from the input data in both INSERT and UPDATE cases
sql = """insert into LIGHTRAG_DOC_STATUS(workspace,id,content,content_summary,content_length,chunks_count,status,file_path,chunks_list,created_at,updated_at)
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
sql = """insert into LIGHTRAG_DOC_STATUS(workspace,id,content_summary,content_length,chunks_count,status,file_path,chunks_list,track_id,metadata,error_msg,created_at,updated_at)
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
on conflict(id,workspace) do update set
content = EXCLUDED.content,
content_summary = EXCLUDED.content_summary,
content_length = EXCLUDED.content_length,
chunks_count = EXCLUDED.chunks_count,
status = EXCLUDED.status,
file_path = EXCLUDED.file_path,
chunks_list = EXCLUDED.chunks_list,
track_id = EXCLUDED.track_id,
metadata = EXCLUDED.metadata,
error_msg = EXCLUDED.error_msg,
created_at = EXCLUDED.created_at,
updated_at = EXCLUDED.updated_at"""
for k, v in data.items():
@ -1862,19 +2283,23 @@ class PGDocStatusStorage(DocStatusStorage):
created_at = parse_datetime(v.get("created_at"))
updated_at = parse_datetime(v.get("updated_at"))
# chunks_count and chunks_list are optional
# chunks_count, chunks_list, track_id, metadata, and error_msg are optional
await self.db.execute(
sql,
{
"workspace": self.db.workspace,
"id": k,
"content": v["content"],
"content_summary": v["content_summary"],
"content_length": v["content_length"],
"chunks_count": v["chunks_count"] if "chunks_count" in v else -1,
"status": v["status"],
"file_path": v["file_path"],
"chunks_list": json.dumps(v.get("chunks_list", [])),
"track_id": v.get("track_id"), # Add track_id support
"metadata": json.dumps(
v.get("metadata", {})
), # Add metadata support
"error_msg": v.get("error_msg"), # Add error_msg support
"created_at": created_at, # Use the converted datetime object
"updated_at": updated_at, # Use the converted datetime object
},
@ -2864,7 +3289,7 @@ class PGGraphStorage(BaseGraphStorage):
query = f"""
SELECT * FROM cypher('{self.graph_name}', $$
UNWIND {chunk_ids_str} AS chunk_id
MATCH (a:base)-[r]-(b:base)
MATCH ()-[r]-()
WHERE r.source_id IS NOT NULL AND chunk_id IN split(r.source_id, '{GRAPH_FIELD_SEP}')
RETURN DISTINCT r, startNode(r) AS source, endNode(r) AS target
$$) AS (edge agtype, source agtype, target agtype);
@ -3368,13 +3793,15 @@ TABLES = {
"ddl": """CREATE TABLE LIGHTRAG_DOC_STATUS (
workspace varchar(255) NOT NULL,
id varchar(255) NOT NULL,
content TEXT NULL,
content_summary varchar(255) NULL,
content_length int4 NULL,
chunks_count int4 NULL,
status varchar(64) NULL,
file_path TEXT NULL,
chunks_list JSONB NULL DEFAULT '[]'::jsonb,
track_id varchar(255) NULL,
metadata JSONB NULL DEFAULT '{}'::jsonb,
error_msg TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT LIGHTRAG_DOC_STATUS_PK PRIMARY KEY (workspace, id)

View File

@ -786,15 +786,16 @@ class RedisDocStatusStorage(DocStatusStorage):
# Make a copy of the data to avoid modifying the original
data = doc_data.copy()
# If content is missing, use content_summary as content
if (
"content" not in data
and "content_summary" in data
):
data["content"] = data["content_summary"]
# Remove deprecated content field if it exists
data.pop("content", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
result[doc_id] = DocProcessingStatus(**data)
except (json.JSONDecodeError, KeyError) as e:
@ -810,6 +811,62 @@ class RedisDocStatusStorage(DocStatusStorage):
return result
async def get_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get all documents with a specific track_id"""
result = {}
async with self._get_redis_connection() as redis:
try:
# Use SCAN to iterate through all keys in the namespace
cursor = 0
while True:
cursor, keys = await redis.scan(
cursor, match=f"{self.namespace}:*", count=1000
)
if keys:
# Get all values in batch
pipe = redis.pipeline()
for key in keys:
pipe.get(key)
values = await pipe.execute()
# Filter by track_id and create DocProcessingStatus objects
for key, value in zip(keys, values):
if value:
try:
doc_data = json.loads(value)
if doc_data.get("track_id") == track_id:
# Extract document ID from key
doc_id = key.split(":", 1)[1]
# Make a copy of the data to avoid modifying the original
data = doc_data.copy()
# Remove deprecated content field if it exists
data.pop("content", None)
# If file_path is not in data, use document id as file path
if "file_path" not in data:
data["file_path"] = "no-file-path"
# Ensure new fields exist with default values
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
result[doc_id] = DocProcessingStatus(**data)
except (json.JSONDecodeError, KeyError) as e:
logger.error(
f"Error processing document {key}: {e}"
)
continue
if cursor == 0:
break
except Exception as e:
logger.error(f"Error getting docs by track_id: {e}")
return result
async def index_done_callback(self) -> None:
"""Redis handles persistence automatically"""
pass
@ -862,6 +919,138 @@ class RedisDocStatusStorage(DocStatusStorage):
f"Deleted {deleted_count} of {len(doc_ids)} doc status entries from {self.namespace}"
)
async def get_docs_paginated(
self,
status_filter: DocStatus | None = None,
page: int = 1,
page_size: int = 50,
sort_field: str = "updated_at",
sort_direction: str = "desc",
) -> tuple[list[tuple[str, DocProcessingStatus]], int]:
"""Get documents with pagination support
Args:
status_filter: Filter by document status, None for all statuses
page: Page number (1-based)
page_size: Number of documents per page (10-200)
sort_field: Field to sort by ('created_at', 'updated_at', 'id')
sort_direction: Sort direction ('asc' or 'desc')
Returns:
Tuple of (list of (doc_id, DocProcessingStatus) tuples, total_count)
"""
# Validate parameters
if page < 1:
page = 1
if page_size < 10:
page_size = 10
elif page_size > 200:
page_size = 200
if sort_field not in ["created_at", "updated_at", "id", "file_path"]:
sort_field = "updated_at"
if sort_direction.lower() not in ["asc", "desc"]:
sort_direction = "desc"
# For Redis, we need to load all data and sort/filter in memory
all_docs = []
total_count = 0
async with self._get_redis_connection() as redis:
try:
# Use SCAN to iterate through all keys in the namespace
cursor = 0
while True:
cursor, keys = await redis.scan(
cursor, match=f"{self.namespace}:*", count=1000
)
if keys:
# Get all values in batch
pipe = redis.pipeline()
for key in keys:
pipe.get(key)
values = await pipe.execute()
# Process documents
for key, value in zip(keys, values):
if value:
try:
doc_data = json.loads(value)
# Apply status filter
if (
status_filter is not None
and doc_data.get("status")
!= status_filter.value
):
continue
# Extract document ID from key
doc_id = key.split(":", 1)[1]
# Prepare document data
data = doc_data.copy()
data.pop("content", None)
if "file_path" not in data:
data["file_path"] = "no-file-path"
if "metadata" not in data:
data["metadata"] = {}
if "error_msg" not in data:
data["error_msg"] = None
# Calculate sort key for sorting (but don't add to data)
if sort_field == "id":
sort_key = doc_id
else:
sort_key = data.get(sort_field, "")
doc_status = DocProcessingStatus(**data)
all_docs.append((doc_id, doc_status, sort_key))
except (json.JSONDecodeError, KeyError) as e:
logger.error(
f"Error processing document {key}: {e}"
)
continue
if cursor == 0:
break
except Exception as e:
logger.error(f"Error getting paginated docs: {e}")
return [], 0
# Sort documents using the separate sort key
reverse_sort = sort_direction.lower() == "desc"
all_docs.sort(key=lambda x: x[2], reverse=reverse_sort)
# Remove sort key from tuples and keep only (doc_id, doc_status)
all_docs = [(doc_id, doc_status) for doc_id, doc_status, _ in all_docs]
total_count = len(all_docs)
# Apply pagination
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
paginated_docs = all_docs[start_idx:end_idx]
return paginated_docs, total_count
async def get_all_status_counts(self) -> dict[str, int]:
"""Get counts of documents in each status for all documents
Returns:
Dictionary mapping status names to counts, including 'all' field
"""
counts = await self.get_status_counts()
# Add 'all' field with total count
total_count = sum(counts.values())
counts["all"] = total_count
return counts
async def drop(self) -> dict[str, str]:
"""Drop all document status data from storage and clean up resources"""
try:

View File

@ -31,6 +31,11 @@ from lightrag.constants import (
DEFAULT_MAX_TOTAL_TOKENS,
DEFAULT_COSINE_THRESHOLD,
DEFAULT_RELATED_CHUNK_NUMBER,
DEFAULT_MIN_RERANK_SCORE,
DEFAULT_SUMMARY_MAX_TOKENS,
DEFAULT_MAX_ASYNC,
DEFAULT_MAX_PARALLEL_INSERT,
DEFAULT_MAX_GRAPH_NODES,
)
from lightrag.utils import get_env_value
@ -39,6 +44,7 @@ from lightrag.kg import (
verify_storage_implementation,
)
from lightrag.kg.shared_storage import (
get_namespace_data,
get_pipeline_status_lock,
@ -56,6 +62,7 @@ from .base import (
StorageNameSpace,
StoragesStatus,
DeletionResult,
OllamaServerInfos,
)
from .namespace import NameSpace
from .operate import (
@ -74,12 +81,12 @@ from .utils import (
EmbeddingFunc,
always_get_an_event_loop,
compute_mdhash_id,
convert_response_to_json,
lazy_external_import,
priority_limit_async_func_call,
get_content_summary,
clean_text,
check_storage_env_vars,
generate_track_id,
logger,
)
from .types import KnowledgeGraph
@ -269,10 +276,14 @@ class LightRAG:
llm_model_name: str = field(default="gpt-4o-mini")
"""Name of the LLM model used for generating responses."""
llm_model_max_token_size: int = field(default=int(os.getenv("MAX_TOKENS", 32000)))
summary_max_tokens: int = field(
default=int(os.getenv("MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS))
)
"""Maximum number of tokens allowed per LLM response."""
llm_model_max_async: int = field(default=int(os.getenv("MAX_ASYNC", 4)))
llm_model_max_async: int = field(
default=int(os.getenv("MAX_ASYNC", DEFAULT_MAX_ASYNC))
)
"""Maximum number of concurrent LLM calls."""
llm_model_kwargs: dict[str, Any] = field(default_factory=dict)
@ -284,6 +295,11 @@ class LightRAG:
rerank_model_func: Callable[..., object] | None = field(default=None)
"""Function for reranking retrieved documents. All rerank configurations (model name, API keys, top_k, etc.) should be included in this function. Optional."""
min_rerank_score: float = field(
default=get_env_value("MIN_RERANK_SCORE", DEFAULT_MIN_RERANK_SCORE, float)
)
"""Minimum rerank score threshold for filtering chunks after reranking."""
# Storage
# ---
@ -299,10 +315,14 @@ class LightRAG:
# Extensions
# ---
max_parallel_insert: int = field(default=int(os.getenv("MAX_PARALLEL_INSERT", 2)))
max_parallel_insert: int = field(
default=int(os.getenv("MAX_PARALLEL_INSERT", DEFAULT_MAX_PARALLEL_INSERT))
)
"""Maximum number of parallel insert operations."""
max_graph_nodes: int = field(default=get_env_value("MAX_GRAPH_NODES", 1000, int))
max_graph_nodes: int = field(
default=get_env_value("MAX_GRAPH_NODES", DEFAULT_MAX_GRAPH_NODES, int)
)
"""Maximum number of graph nodes to return in knowledge graph queries."""
addon_params: dict[str, Any] = field(
@ -320,19 +340,13 @@ class LightRAG:
# Storages Management
# ---
convert_response_to_json_func: Callable[[str], dict[str, Any]] = field(
default_factory=lambda: convert_response_to_json
)
"""
Custom function for converting LLM responses to JSON format.
The default function is :func:`.utils.convert_response_to_json`.
"""
cosine_better_than_threshold: float = field(
default=float(os.getenv("COSINE_THRESHOLD", 0.2))
)
ollama_server_infos: Optional[OllamaServerInfos] = field(default=None)
"""Configuration for Ollama server information."""
_storages_status: StoragesStatus = field(default=StoragesStatus.NOT_CREATED)
def __post_init__(self):
@ -394,6 +408,10 @@ class LightRAG:
else:
self.tokenizer = TiktokenTokenizer()
# Initialize ollama_server_infos if not provided
if self.ollama_server_infos is None:
self.ollama_server_infos = OllamaServerInfos()
# Fix global_config now
global_config = asdict(self)
@ -548,7 +566,7 @@ class LightRAG:
await asyncio.gather(*tasks)
self._storages_status = StoragesStatus.INITIALIZED
logger.debug("Initialized Storages")
logger.debug("All storage types initialized")
async def finalize_storages(self):
"""Asynchronously finalize the storages"""
@ -605,9 +623,28 @@ class LightRAG:
)
def _get_storage_class(self, storage_name: str) -> Callable[..., Any]:
import_path = STORAGES[storage_name]
storage_class = lazy_external_import(import_path, storage_name)
return storage_class
# Direct imports for default storage implementations
if storage_name == "JsonKVStorage":
from lightrag.kg.json_kv_impl import JsonKVStorage
return JsonKVStorage
elif storage_name == "NanoVectorDBStorage":
from lightrag.kg.nano_vector_db_impl import NanoVectorDBStorage
return NanoVectorDBStorage
elif storage_name == "NetworkXStorage":
from lightrag.kg.networkx_impl import NetworkXStorage
return NetworkXStorage
elif storage_name == "JsonDocStatusStorage":
from lightrag.kg.json_doc_status_impl import JsonDocStatusStorage
return JsonDocStatusStorage
else:
# Fallback to dynamic import for other storage implementations
import_path = STORAGES[storage_name]
storage_class = lazy_external_import(import_path, storage_name)
return storage_class
def insert(
self,
@ -616,7 +653,8 @@ class LightRAG:
split_by_character_only: bool = False,
ids: str | list[str] | None = None,
file_paths: str | list[str] | None = None,
) -> None:
track_id: str | None = None,
) -> str:
"""Sync Insert documents with checkpoint support
Args:
@ -627,11 +665,20 @@ class LightRAG:
split_by_character is None, this parameter is ignored.
ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: single string of the file path or list of file paths, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated
Returns:
str: tracking ID for monitoring processing status
"""
loop = always_get_an_event_loop()
loop.run_until_complete(
return loop.run_until_complete(
self.ainsert(
input, split_by_character, split_by_character_only, ids, file_paths
input,
split_by_character,
split_by_character_only,
ids,
file_paths,
track_id,
)
)
@ -642,7 +689,8 @@ class LightRAG:
split_by_character_only: bool = False,
ids: str | list[str] | None = None,
file_paths: str | list[str] | None = None,
) -> None:
track_id: str | None = None,
) -> str:
"""Async Insert documents with checkpoint support
Args:
@ -653,12 +701,22 @@ class LightRAG:
split_by_character is None, this parameter is ignored.
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: list of file paths corresponding to each document, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated
Returns:
str: tracking ID for monitoring processing status
"""
await self.apipeline_enqueue_documents(input, ids, file_paths)
# Generate track_id if not provided
if track_id is None:
track_id = generate_track_id("insert")
await self.apipeline_enqueue_documents(input, ids, file_paths, track_id)
await self.apipeline_process_enqueue_documents(
split_by_character, split_by_character_only
)
return track_id
# TODO: deprecated, use insert instead
def insert_custom_chunks(
self,
@ -736,7 +794,8 @@ class LightRAG:
input: str | list[str],
ids: list[str] | None = None,
file_paths: str | list[str] | None = None,
) -> None:
track_id: str | None = None,
) -> str:
"""
Pipeline for Processing Documents
@ -750,7 +809,14 @@ class LightRAG:
input: Single document string or list of document strings
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: list of file paths corresponding to each document, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated with "enqueue" prefix
Returns:
str: tracking ID for monitoring processing status
"""
# Generate track_id if not provided
if track_id is None or track_id.strip() == "":
track_id = generate_track_id("enqueue")
if isinstance(input, str):
input = [input]
if isinstance(ids, str):
@ -820,11 +886,10 @@ class LightRAG:
for content, (id_, file_path) in unique_contents.items()
}
# 3. Generate document initial status
# 3. Generate document initial status (without content)
new_docs: dict[str, Any] = {
id_: {
"status": DocStatus.PENDING,
"content": content_data["content"],
"content_summary": get_content_summary(content_data["content"]),
"content_length": len(content_data["content"]),
"created_at": datetime.now(timezone.utc).isoformat(),
@ -832,6 +897,7 @@ class LightRAG:
"file_path": content_data[
"file_path"
], # Store file path in document status
"track_id": track_id, # Store track_id in document status
}
for id_, content_data in contents.items()
}
@ -864,10 +930,20 @@ class LightRAG:
logger.info("No new unique documents were found.")
return
# 5. Store status document
# 5. Store document content in full_docs and status in doc_status
# Store full document content separately
full_docs_data = {
doc_id: {"content": contents[doc_id]["content"]}
for doc_id in new_docs.keys()
}
await self.full_docs.upsert(full_docs_data)
# Store document status (without content)
await self.doc_status.upsert(new_docs)
logger.info(f"Stored {len(new_docs)} new unique documents")
return track_id
async def apipeline_process_enqueue_documents(
self,
split_by_character: str | None = None,
@ -1006,6 +1082,14 @@ class LightRAG:
pipeline_status["latest_message"] = log_message
pipeline_status["history_messages"].append(log_message)
# Get document content from full_docs
content_data = await self.full_docs.get_by_id(doc_id)
if not content_data:
raise Exception(
f"Document content not found in full_docs for doc_id: {doc_id}"
)
content = content_data["content"]
# Generate chunks from document
chunks: dict[str, Any] = {
compute_mdhash_id(dp["content"], prefix="chunk-"): {
@ -1016,7 +1100,7 @@ class LightRAG:
}
for dp in self.chunking_func(
self.tokenizer,
status_doc.content,
content,
split_by_character,
split_by_character_only,
self.chunk_overlap_token_size,
@ -1027,6 +1111,9 @@ class LightRAG:
if not chunks:
logger.warning("No document chunks to process")
# Record processing start time
processing_start_time = int(time.time())
# Process document in two stages
# Stage 1: Process text chunks and docs (parallel execution)
doc_status_task = asyncio.create_task(
@ -1038,7 +1125,6 @@ class LightRAG:
"chunks_list": list(
chunks.keys()
), # Save chunks list
"content": status_doc.content,
"content_summary": status_doc.content_summary,
"content_length": status_doc.content_length,
"created_at": status_doc.created_at,
@ -1046,6 +1132,10 @@ class LightRAG:
timezone.utc
).isoformat(),
"file_path": file_path,
"track_id": status_doc.track_id, # Preserve existing track_id
"metadata": {
"processing_start_time": processing_start_time
},
}
}
)
@ -1053,11 +1143,6 @@ class LightRAG:
chunks_vdb_task = asyncio.create_task(
self.chunks_vdb.upsert(chunks)
)
full_docs_task = asyncio.create_task(
self.full_docs.upsert(
{doc_id: {"content": status_doc.content}}
)
)
text_chunks_task = asyncio.create_task(
self.text_chunks.upsert(chunks)
)
@ -1066,7 +1151,6 @@ class LightRAG:
first_stage_tasks = [
doc_status_task,
chunks_vdb_task,
full_docs_task,
text_chunks_task,
]
entity_relation_task = None
@ -1109,13 +1193,15 @@ class LightRAG:
if self.llm_response_cache:
await self.llm_response_cache.index_done_callback()
# Record processing end time for failed case
processing_end_time = int(time.time())
# Update document status to failed
await self.doc_status.upsert(
{
doc_id: {
"status": DocStatus.FAILED,
"error": str(e),
"content": status_doc.content,
"error_msg": str(e),
"content_summary": status_doc.content_summary,
"content_length": status_doc.content_length,
"created_at": status_doc.created_at,
@ -1123,6 +1209,11 @@ class LightRAG:
timezone.utc
).isoformat(),
"file_path": file_path,
"track_id": status_doc.track_id, # Preserve existing track_id
"metadata": {
"processing_start_time": processing_start_time,
"processing_end_time": processing_end_time,
},
}
}
)
@ -1146,6 +1237,9 @@ class LightRAG:
file_path=file_path,
)
# Record processing end time
processing_end_time = int(time.time())
await self.doc_status.upsert(
{
doc_id: {
@ -1154,7 +1248,6 @@ class LightRAG:
"chunks_list": list(
chunks.keys()
), # 保留 chunks_list
"content": status_doc.content,
"content_summary": status_doc.content_summary,
"content_length": status_doc.content_length,
"created_at": status_doc.created_at,
@ -1162,6 +1255,11 @@ class LightRAG:
timezone.utc
).isoformat(),
"file_path": file_path,
"track_id": status_doc.track_id, # Preserve existing track_id
"metadata": {
"processing_start_time": processing_start_time,
"processing_end_time": processing_end_time,
},
}
}
)
@ -1195,18 +1293,25 @@ class LightRAG:
if self.llm_response_cache:
await self.llm_response_cache.index_done_callback()
# Record processing end time for failed case
processing_end_time = int(time.time())
# Update document status to failed
await self.doc_status.upsert(
{
doc_id: {
"status": DocStatus.FAILED,
"error": str(e),
"content": status_doc.content,
"error_msg": str(e),
"content_summary": status_doc.content_summary,
"content_length": status_doc.content_length,
"created_at": status_doc.created_at,
"updated_at": datetime.now().isoformat(),
"file_path": file_path,
"track_id": status_doc.track_id, # Preserve existing track_id
"metadata": {
"processing_start_time": processing_start_time,
"processing_end_time": processing_end_time,
},
}
}
)
@ -2151,6 +2256,19 @@ class LightRAG:
"""
return await self.doc_status.get_status_counts()
async def aget_docs_by_track_id(
self, track_id: str
) -> dict[str, DocProcessingStatus]:
"""Get documents by track_id
Args:
track_id: The tracking ID to search for
Returns:
Dict with document id as keys and document status as values
"""
return await self.doc_status.get_docs_by_track_id(track_id)
async def get_entity_info(
self, entity_name: str, include_vector_data: bool = False
) -> dict[str, str | None | dict[str, str]]:

View File

@ -58,7 +58,6 @@ rag = LightRAG(
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=1536,
max_token_size=8192,
func=lambda texts: llama_index_embed(
texts,
embed_model=OpenAIEmbedding(
@ -114,7 +113,6 @@ rag = LightRAG(
llm_model_func=llm_model_func,
embedding_func=EmbeddingFunc(
embedding_dim=1536,
max_token_size=8192,
func=lambda texts: llama_index_embed(
texts,
embed_model=LiteLLMEmbedding(
@ -143,7 +141,6 @@ LITELLM_KEY=your-litellm-key
# Model Configuration
LLM_MODEL=gpt-4
EMBEDDING_MODEL=text-embedding-3-large
EMBEDDING_MAX_TOKEN_SIZE=8192
```
### Key Differences

View File

@ -23,7 +23,6 @@ from tenacity import (
from lightrag.utils import (
wrap_embedding_func_with_attrs,
locate_json_string_body_from_string,
safe_unicode_decode,
)
@ -108,7 +107,7 @@ async def azure_openai_complete_if_cache(
async def azure_openai_complete(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
keyword_extraction = kwargs.pop("keyword_extraction", None)
kwargs.pop("keyword_extraction", None)
result = await azure_openai_complete_if_cache(
os.getenv("LLM_MODEL", "gpt-4o-mini"),
prompt,
@ -116,12 +115,10 @@ async def azure_openai_complete(
history_messages=history_messages,
**kwargs,
)
if keyword_extraction: # TODO: use JSON API
return locate_json_string_body_from_string(result)
return result
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8191)
@wrap_embedding_func_with_attrs(embedding_dim=1536)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),

View File

@ -15,10 +15,6 @@ from tenacity import (
retry_if_exception_type,
)
from lightrag.utils import (
locate_json_string_body_from_string,
)
class BedrockError(Exception):
"""Generic error for issues related to Amazon Bedrock"""
@ -96,7 +92,7 @@ async def bedrock_complete_if_cache(
async def bedrock_complete(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
keyword_extraction = kwargs.pop("keyword_extraction", None)
kwargs.pop("keyword_extraction", None)
model_name = kwargs["hashing_kv"].global_config["llm_model_name"]
result = await bedrock_complete_if_cache(
model_name,
@ -105,12 +101,10 @@ async def bedrock_complete(
history_messages=history_messages,
**kwargs,
)
if keyword_extraction: # TODO: use JSON API
return locate_json_string_body_from_string(result)
return result
# @wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192)
# @wrap_embedding_func_with_attrs(embedding_dim=1024)
# @retry(
# stop=stop_after_attempt(3),
# wait=wait_exponential(multiplier=1, min=4, max=10),

View File

@ -0,0 +1,446 @@
"""
Module that implements containers for specific LLM bindings.
This module provides container implementations for various Large Language Model
bindings and integrations.
"""
from argparse import ArgumentParser, Namespace
import argparse
from dataclasses import asdict, dataclass
from typing import Any, ClassVar
from lightrag.utils import get_env_value
# =============================================================================
# BindingOptions Base Class
# =============================================================================
#
# The BindingOptions class serves as the foundation for all LLM provider bindings
# in LightRAG. It provides a standardized framework for:
#
# 1. Configuration Management:
# - Defines how each LLM provider's configuration parameters are structured
# - Handles default values and type information for each parameter
# - Maps configuration options to command-line arguments and environment variables
#
# 2. Environment Integration:
# - Automatically generates environment variable names from binding parameters
# - Provides methods to create sample .env files for easy configuration
# - Supports configuration via environment variables with fallback to defaults
#
# 3. Command-Line Interface:
# - Dynamically generates command-line arguments for all registered bindings
# - Maintains consistent naming conventions across different LLM providers
# - Provides help text and type validation for each configuration option
#
# 4. Extensibility:
# - Uses class introspection to automatically discover all binding subclasses
# - Requires minimal boilerplate code when adding new LLM provider bindings
# - Maintains separation of concerns between different provider configurations
#
# This design pattern ensures that adding support for a new LLM provider requires
# only defining the provider-specific parameters and help text, while the base
# class handles all the common functionality for argument parsing, environment
# variable handling, and configuration management.
#
# Instances of a derived class of BindingOptions can be used to store multiple
# runtime configurations of options for a single LLM provider. using the
# asdict() method to convert the options to a dictionary.
#
# =============================================================================
@dataclass
class BindingOptions:
"""Base class for binding options."""
# mandatory name of binding
_binding_name: ClassVar[str]
# optional help message for each option
_help: ClassVar[dict[str, str]]
@staticmethod
def _all_class_vars(klass: type, include_inherited=True) -> dict[str, Any]:
"""Print class variables, optionally including inherited ones"""
if include_inherited:
# Get all class variables from MRO
vars_dict = {}
for base in reversed(klass.__mro__[:-1]): # Exclude 'object'
vars_dict.update(
{
k: v
for k, v in base.__dict__.items()
if (
not k.startswith("_")
and not callable(v)
and not isinstance(v, classmethod)
)
}
)
else:
# Only direct class variables
vars_dict = {
k: v
for k, v in klass.__dict__.items()
if (
not k.startswith("_")
and not callable(v)
and not isinstance(v, classmethod)
)
}
return vars_dict
@classmethod
def add_args(cls, parser: ArgumentParser):
group = parser.add_argument_group(f"{cls._binding_name} binding options")
for arg_item in cls.args_env_name_type_value():
group.add_argument(
f"--{arg_item['argname']}",
type=arg_item["type"],
default=get_env_value(f"{arg_item['env_name']}", argparse.SUPPRESS),
help=arg_item["help"],
)
@classmethod
def args_env_name_type_value(cls):
args_prefix = f"{cls._binding_name}".replace("_", "-")
env_var_prefix = f"{cls._binding_name}_".upper()
class_vars = {
key: value
for key, value in cls._all_class_vars(cls).items()
if not callable(value) and not key.startswith("_")
}
help = cls._help
for class_var in class_vars:
argdef = {
"argname": f"{args_prefix}-{class_var}",
"env_name": f"{env_var_prefix}{class_var.upper()}",
"type": type(class_vars[class_var]),
"default": class_vars[class_var],
"help": f"{cls._binding_name} -- " + help.get(class_var, ""),
}
yield argdef
@classmethod
def generate_dot_env_sample(cls):
from io import StringIO
sample_top = (
"#" * 80
+ "\n"
+ (
"# Autogenerated .env entries list for LightRAG binding options\n"
"#\n"
"# To generate run:\n"
"# $ python -m lightrag.llm.binding_options\n"
)
+ "#" * 80
+ "\n"
)
sample_bottom = (
("#\n# End of .env entries for LightRAG binding options\n")
+ "#" * 80
+ "\n"
)
sample_stream = StringIO()
sample_stream.write(sample_top)
for klass in cls.__subclasses__():
for arg_item in klass.args_env_name_type_value():
if arg_item["help"]:
sample_stream.write(f"# {arg_item['help']}\n")
sample_stream.write(
f"# {arg_item['env_name']}={arg_item['default']}\n\n"
)
sample_stream.write(sample_bottom)
return sample_stream.getvalue()
@classmethod
def options_dict(cls, args: Namespace) -> dict[str, Any]:
"""
Extract options dictionary for a specific binding from parsed arguments.
This method filters the parsed command-line arguments to return only those
that belong to the specific binding class. It removes the binding prefix
from argument names to create a clean options dictionary.
Args:
args (Namespace): Parsed command-line arguments containing all binding options
Returns:
dict[str, Any]: Dictionary mapping option names (without prefix) to their values
Example:
If args contains {'ollama_num_ctx': 512, 'other_option': 'value'}
and this is called on OllamaOptions, it returns {'num_ctx': 512}
"""
prefix = cls._binding_name + "_"
skipchars = len(prefix)
options = {
key[skipchars:]: value
for key, value in vars(args).items()
if key.startswith(prefix)
}
return options
def asdict(self) -> dict[str, Any]:
"""
Convert an instance of binding options to a dictionary.
This method uses dataclasses.asdict() to convert the dataclass instance
into a dictionary representation, including all its fields and values.
Returns:
dict[str, Any]: Dictionary representation of the binding options instance
"""
return asdict(self)
# =============================================================================
# Binding Options for Different LLM Providers
# =============================================================================
#
# This section contains dataclass definitions for various LLM provider options.
# Each binding option class inherits from BindingOptions and defines:
# - _binding_name: Unique identifier for the binding
# - Configuration parameters with default values
# - _help: Dictionary mapping parameter names to help descriptions
#
# To add a new binding:
# 1. Create a new dataclass inheriting from BindingOptions
# 2. Set the _binding_name class variable
# 3. Define configuration parameters as class attributes
# 4. Add corresponding help strings in the _help dictionary
#
# =============================================================================
# =============================================================================
# Binding Options for Ollama
# =============================================================================
#
# Ollama binding options provide configuration for the Ollama local LLM server.
# These options control model behavior, sampling parameters, hardware utilization,
# and performance settings. The parameters are based on Ollama's API specification
# and provide fine-grained control over model inference and generation.
#
# The _OllamaOptionsMixin defines the complete set of available options, while
# OllamaEmbeddingOptions and OllamaLLMOptions provide specialized configurations
# for embedding and language model tasks respectively.
# =============================================================================
@dataclass
class _OllamaOptionsMixin:
"""Options for Ollama bindings."""
# Core context and generation parameters
num_ctx: int = 32768 # Context window size (number of tokens)
num_predict: int = 128 # Maximum number of tokens to predict
num_keep: int = 0 # Number of tokens to keep from the initial prompt
seed: int = -1 # Random seed for generation (-1 for random)
# Sampling parameters
temperature: float = 0.8 # Controls randomness (0.0-2.0)
top_k: int = 40 # Top-k sampling parameter
top_p: float = 0.9 # Top-p (nucleus) sampling parameter
tfs_z: float = 1.0 # Tail free sampling parameter
typical_p: float = 1.0 # Typical probability mass
min_p: float = 0.0 # Minimum probability threshold
# Repetition control
repeat_last_n: int = 64 # Number of tokens to consider for repetition penalty
repeat_penalty: float = 1.1 # Penalty for repetition
presence_penalty: float = 0.0 # Penalty for token presence
frequency_penalty: float = 0.0 # Penalty for token frequency
# Mirostat sampling
mirostat: int = (
# Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)
0
)
mirostat_tau: float = 5.0 # Mirostat target entropy
mirostat_eta: float = 0.1 # Mirostat learning rate
# Hardware and performance parameters
numa: bool = False # Enable NUMA optimization
num_batch: int = 512 # Batch size for processing
num_gpu: int = -1 # Number of GPUs to use (-1 for auto)
main_gpu: int = 0 # Main GPU index
low_vram: bool = False # Optimize for low VRAM
num_thread: int = 0 # Number of CPU threads (0 for auto)
# Memory and model parameters
f16_kv: bool = True # Use half-precision for key/value cache
logits_all: bool = False # Return logits for all tokens
vocab_only: bool = False # Only load vocabulary
use_mmap: bool = True # Use memory mapping for model files
use_mlock: bool = False # Lock model in memory
embedding_only: bool = False # Only use for embeddings
# Output control
penalize_newline: bool = True # Penalize newline tokens
stop: str = "" # Stop sequences (comma-separated)
# optional help strings
_help: ClassVar[dict[str, str]] = {
"num_ctx": "Context window size (number of tokens)",
"num_predict": "Maximum number of tokens to predict",
"num_keep": "Number of tokens to keep from the initial prompt",
"seed": "Random seed for generation (-1 for random)",
"temperature": "Controls randomness (0.0-2.0, higher = more creative)",
"top_k": "Top-k sampling parameter (0 = disabled)",
"top_p": "Top-p (nucleus) sampling parameter (0.0-1.0)",
"tfs_z": "Tail free sampling parameter (1.0 = disabled)",
"typical_p": "Typical probability mass (1.0 = disabled)",
"min_p": "Minimum probability threshold (0.0 = disabled)",
"repeat_last_n": "Number of tokens to consider for repetition penalty",
"repeat_penalty": "Penalty for repetition (1.0 = no penalty)",
"presence_penalty": "Penalty for token presence (-2.0 to 2.0)",
"frequency_penalty": "Penalty for token frequency (-2.0 to 2.0)",
"mirostat": "Mirostat sampling algorithm (0=disabled, 1=Mirostat 1.0, 2=Mirostat 2.0)",
"mirostat_tau": "Mirostat target entropy",
"mirostat_eta": "Mirostat learning rate",
"numa": "Enable NUMA optimization",
"num_batch": "Batch size for processing",
"num_gpu": "Number of GPUs to use (-1 for auto)",
"main_gpu": "Main GPU index",
"low_vram": "Optimize for low VRAM",
"num_thread": "Number of CPU threads (0 for auto)",
"f16_kv": "Use half-precision for key/value cache",
"logits_all": "Return logits for all tokens",
"vocab_only": "Only load vocabulary",
"use_mmap": "Use memory mapping for model files",
"use_mlock": "Lock model in memory",
"embedding_only": "Only use for embeddings",
"penalize_newline": "Penalize newline tokens",
"stop": "Stop sequences (comma-separated string)",
}
# =============================================================================
# Ollama Binding Options - Specialized Configurations
# =============================================================================
#
# This section defines specialized binding option classes for different Ollama
# use cases. Both classes inherit from OllamaOptionsMixin to share the complete
# set of Ollama configuration parameters, while providing distinct binding names
# for command-line argument generation and environment variable handling.
#
# OllamaEmbeddingOptions: Specialized for embedding tasks
# OllamaLLMOptions: Specialized for language model/chat tasks
#
# Each class maintains its own binding name prefix, allowing users to configure
# embedding and LLM options independently when both are used in the same application.
# =============================================================================
@dataclass
class OllamaEmbeddingOptions(_OllamaOptionsMixin, BindingOptions):
"""Options for Ollama embeddings with specialized configuration for embedding tasks."""
# mandatory name of binding
_binding_name: ClassVar[str] = "ollama_embedding"
@dataclass
class OllamaLLMOptions(_OllamaOptionsMixin, BindingOptions):
"""Options for Ollama LLM with specialized configuration for LLM tasks."""
# mandatory name of binding
_binding_name: ClassVar[str] = "ollama_llm"
# =============================================================================
# Additional LLM Provider Bindings
# =============================================================================
#
# This section is where you can add binding options for other LLM providers.
# Each new binding should follow the same pattern as the Ollama bindings above:
#
# 1. Create a dataclass that inherits from BindingOptions
# 2. Set a unique _binding_name class variable (e.g., "openai", "anthropic")
# 3. Define configuration parameters as class attributes with default values
# 4. Add a _help class variable with descriptions for each parameter
#
# Example template for a new provider:
#
# @dataclass
# class NewProviderOptions(BindingOptions):
# """Options for NewProvider LLM binding."""
#
# _binding_name: ClassVar[str] = "newprovider"
#
# # Configuration parameters
# api_key: str = ""
# max_tokens: int = 1000
# model: str = "default-model"
#
# # Help descriptions
# _help: ClassVar[dict[str, str]] = {
# "api_key": "API key for authentication",
# "max_tokens": "Maximum tokens to generate",
# "model": "Model name to use",
# }
#
# =============================================================================
# TODO: Add binding options for additional LLM providers here
# Common providers to consider: OpenAI, Anthropic, Cohere, Hugging Face, etc.
# =============================================================================
# Main Section - For Testing and Sample Generation
# =============================================================================
#
# When run as a script, this module:
# 1. Generates and prints a sample .env file with all binding options
# 2. If "test" argument is provided, demonstrates argument parsing with Ollama binding
#
# Usage:
# python -m lightrag.llm.binding_options # Generate .env sample
# python -m lightrag.llm.binding_options test # Test argument parsing
#
# =============================================================================
if __name__ == "__main__":
import sys
import dotenv
from io import StringIO
print(BindingOptions.generate_dot_env_sample())
env_strstream = StringIO(
("OLLAMA_LLM_TEMPERATURE=0.1\nOLLAMA_EMBEDDING_TEMPERATURE=0.2\n")
)
# Load environment variables from .env file
dotenv.load_dotenv(stream=env_strstream)
if len(sys.argv) > 1 and sys.argv[1] == "test":
parser = ArgumentParser(description="Test Ollama binding")
OllamaEmbeddingOptions.add_args(parser)
OllamaLLMOptions.add_args(parser)
args = parser.parse_args(
[
"--ollama-embedding-num_ctx",
"1024",
"--ollama-llm-num_ctx",
"2048",
]
)
print(args)
# test LLM options
ollama_options = OllamaLLMOptions.options_dict(args)
print(ollama_options)
print(OllamaLLMOptions(num_ctx=30000).asdict())
# test embedding options
embedding_options = OllamaEmbeddingOptions.options_dict(args)
print(embedding_options)
print(OllamaEmbeddingOptions(**embedding_options).asdict())

View File

@ -24,9 +24,6 @@ from lightrag.exceptions import (
RateLimitError,
APITimeoutError,
)
from lightrag.utils import (
locate_json_string_body_from_string,
)
import torch
import numpy as np
@ -119,7 +116,7 @@ async def hf_model_if_cache(
async def hf_model_complete(
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
) -> str:
keyword_extraction = kwargs.pop("keyword_extraction", None)
kwargs.pop("keyword_extraction", None)
model_name = kwargs["hashing_kv"].global_config["llm_model_name"]
result = await hf_model_if_cache(
model_name,
@ -128,8 +125,6 @@ async def hf_model_complete(
history_messages=history_messages,
**kwargs,
)
if keyword_extraction: # TODO: use JSON API
return locate_json_string_body_from_string(result)
return result

View File

@ -2,45 +2,117 @@ import os
import pipmaster as pm # Pipmaster for dynamic library install
# install specific modules
if not pm.is_installed("lmdeploy"):
pm.install("lmdeploy")
if not pm.is_installed("aiohttp"):
pm.install("aiohttp")
if not pm.is_installed("tenacity"):
pm.install("tenacity")
import numpy as np
import aiohttp
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
from lightrag.utils import wrap_embedding_func_with_attrs, logger
async def fetch_data(url, headers, data):
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data) as response:
if response.status != 200:
error_text = await response.text()
logger.error(f"Jina API error {response.status}: {error_text}")
raise aiohttp.ClientResponseError(
request_info=response.request_info,
history=response.history,
status=response.status,
message=f"Jina API error: {error_text}",
)
response_json = await response.json()
data_list = response_json.get("data", [])
return data_list
@wrap_embedding_func_with_attrs(embedding_dim=2048)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),
retry=(
retry_if_exception_type(aiohttp.ClientError)
| retry_if_exception_type(aiohttp.ClientResponseError)
),
)
async def jina_embed(
texts: list[str],
dimensions: int = 1024,
dimensions: int = 2048,
late_chunking: bool = False,
base_url: str = None,
api_key: str = None,
) -> np.ndarray:
"""Generate embeddings for a list of texts using Jina AI's API.
Args:
texts: List of texts to embed.
dimensions: The embedding dimensions (default: 2048 for jina-embeddings-v4).
late_chunking: Whether to use late chunking.
base_url: Optional base URL for the Jina API.
api_key: Optional Jina API key. If None, uses the JINA_API_KEY environment variable.
Returns:
A numpy array of embeddings, one per input text.
Raises:
aiohttp.ClientError: If there is a connection error with the Jina API.
aiohttp.ClientResponseError: If the Jina API returns an error response.
"""
if api_key:
os.environ["JINA_API_KEY"] = api_key
url = "https://api.jina.ai/v1/embeddings" if not base_url else base_url
if "JINA_API_KEY" not in os.environ:
raise ValueError("JINA_API_KEY environment variable is required")
url = base_url or "https://api.jina.ai/v1/embeddings"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['JINA_API_KEY']}",
}
data = {
"model": "jina-embeddings-v3",
"normalized": True,
"embedding_type": "float",
"dimensions": f"{dimensions}",
"late_chunking": late_chunking,
"model": "jina-embeddings-v4",
"task": "text-matching",
"dimensions": dimensions,
"input": texts,
}
data_list = await fetch_data(url, headers, data)
return np.array([dp["embedding"] for dp in data_list])
# Only add optional parameters if they have non-default values
if late_chunking:
data["late_chunking"] = late_chunking
logger.debug(
f"Jina embedding request: {len(texts)} texts, dimensions: {dimensions}"
)
try:
data_list = await fetch_data(url, headers, data)
if not data_list:
logger.error("Jina API returned empty data list")
raise ValueError("Jina API returned empty data list")
if len(data_list) != len(texts):
logger.error(
f"Jina API returned {len(data_list)} embeddings for {len(texts)} texts"
)
raise ValueError(
f"Jina API returned {len(data_list)} embeddings for {len(texts)} texts"
)
embeddings = np.array([dp["embedding"] for dp in data_list])
logger.debug(f"Jina embeddings generated: shape {embeddings.shape}")
return embeddings
except Exception as e:
logger.error(f"Jina embedding error: {e}")
raise

View File

@ -21,7 +21,6 @@ from tenacity import (
)
from lightrag.utils import (
wrap_embedding_func_with_attrs,
locate_json_string_body_from_string,
)
from lightrag.exceptions import (
APIConnectionError,
@ -157,7 +156,7 @@ async def llama_index_complete(
if history_messages is None:
history_messages = []
keyword_extraction = kwargs.pop("keyword_extraction", None)
kwargs.pop("keyword_extraction", None)
result = await llama_index_complete_if_cache(
kwargs.get("llm_instance"),
prompt,
@ -165,12 +164,10 @@ async def llama_index_complete(
history_messages=history_messages,
**kwargs,
)
if keyword_extraction:
return locate_json_string_body_from_string(result)
return result
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
@wrap_embedding_func_with_attrs(embedding_dim=1536)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),

View File

@ -59,7 +59,7 @@ async def lollms_model_if_cache(
"personality": kwargs.get("personality", -1),
"n_predict": kwargs.get("n_predict", None),
"stream": stream,
"temperature": kwargs.get("temperature", 0.1),
"temperature": kwargs.get("temperature", 0.8),
"top_k": kwargs.get("top_k", 50),
"top_p": kwargs.get("top_p", 0.95),
"repeat_penalty": kwargs.get("repeat_penalty", 0.8),

View File

@ -33,7 +33,7 @@ from lightrag.utils import (
import numpy as np
@wrap_embedding_func_with_attrs(embedding_dim=2048, max_token_size=512)
@wrap_embedding_func_with_attrs(embedding_dim=2048)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),

View File

@ -50,7 +50,7 @@ async def _ollama_model_if_cache(
kwargs.pop("max_tokens", None)
# kwargs.pop("response_format", None) # allow json
host = kwargs.pop("host", None)
timeout = kwargs.pop("timeout", None) or 600 # Default timeout 600s
timeout = kwargs.pop("timeout", None)
kwargs.pop("hashing_kv", None)
api_key = kwargs.pop("api_key", None)
headers = {
@ -146,12 +146,14 @@ async def ollama_embed(texts: list[str], embed_model, **kwargs) -> np.ndarray:
headers["Authorization"] = f"Bearer {api_key}"
host = kwargs.pop("host", None)
timeout = kwargs.pop("timeout", None) or 300 # Default time out 300s
timeout = kwargs.pop("timeout", None)
ollama_client = ollama.AsyncClient(host=host, timeout=timeout, headers=headers)
try:
data = await ollama_client.embed(model=embed_model, input=texts)
options = kwargs.pop("options", {})
data = await ollama_client.embed(
model=embed_model, input=texts, options=options
)
return np.array(data["embeddings"])
except Exception as e:
logger.error(f"Error in ollama_embed: {str(e)}")

View File

@ -27,7 +27,6 @@ from tenacity import (
)
from lightrag.utils import (
wrap_embedding_func_with_attrs,
locate_json_string_body_from_string,
safe_unicode_decode,
logger,
)
@ -418,7 +417,7 @@ async def nvidia_openai_complete(
) -> str:
if history_messages is None:
history_messages = []
keyword_extraction = kwargs.pop("keyword_extraction", None)
kwargs.pop("keyword_extraction", None)
result = await openai_complete_if_cache(
"nvidia/llama-3.1-nemotron-70b-instruct", # context length 128k
prompt,
@ -427,12 +426,10 @@ async def nvidia_openai_complete(
base_url="https://integrate.api.nvidia.com/v1",
**kwargs,
)
if keyword_extraction: # TODO: use JSON API
return locate_json_string_body_from_string(result)
return result
@wrap_embedding_func_with_attrs(embedding_dim=1536, max_token_size=8192)
@wrap_embedding_func_with_attrs(embedding_dim=1536)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),

View File

@ -40,7 +40,7 @@ async def siliconcloud_embedding(
texts: list[str],
model: str = "netease-youdao/bce-embedding-base_v1",
base_url: str = "https://api.siliconflow.cn/v1/embeddings",
max_token_size: int = 512,
max_token_size: int = 8192,
api_key: str = None,
) -> np.ndarray:
if api_key and not api_key.startswith("Bearer "):

View File

@ -167,7 +167,7 @@ async def zhipu_complete(
)
@wrap_embedding_func_with_attrs(embedding_dim=1024, max_token_size=8192)
@wrap_embedding_func_with_attrs(embedding_dim=1024)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60),

File diff suppressed because it is too large Load Diff

View File

@ -19,9 +19,9 @@ Use {language} as output language.
---Steps---
1. Identify all entities. For each identified entity, extract the following information:
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name
- entity_type: One of the following types: [{entity_types}]
- entity_description: Comprehensive description of the entity's attributes and activities
- entity_description: Provide a comprehensive description of the entity's attributes and activities *based solely on the information present in the input text*. **Do not infer or hallucinate information not explicitly stated.** If the text provides insufficient information to create a comprehensive description, state "Description not available in text."
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
@ -151,14 +151,14 @@ Output:
"""
PROMPTS["entity_continue_extraction"] = """
MANY entities and relationships were missed in the last extraction.
MANY entities and relationships were missed in the last extraction. Please find only the missing entities and relationships from previous text.
---Remember Steps---
1. Identify all entities. For each identified entity, extract the following information:
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name
- entity_type: One of the following types: [{entity_types}]
- entity_description: Comprehensive description of the entity's attributes and activities
- entity_description: Provide a comprehensive description of the entity's attributes and activities *based solely on the information present in the input text*. **Do not infer or hallucinate information not explicitly stated.** If the text provides insufficient information to create a comprehensive description, state "Description not available in text."
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)
2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
@ -179,7 +179,7 @@ Format the content-level key words as ("content_keywords"{tuple_delimiter}<high_
---Output---
Add them below using the same format:\n
Add new entities and relations below using the same format, and do not include entities and relations that have been previously extracted. :\n
""".strip()
PROMPTS["entity_if_loop_extraction"] = """
@ -226,7 +226,7 @@ When handling relationships with timestamps:
- List up to 5 most important reference sources at the end under "References" section. Clearly indicating whether each source is from Knowledge Graph (KG) or Document Chunks (DC), and include the file path if available, in the following format: [KG/DC] file_path
- If you don't know the answer, just say so.
- Do not make anything up. Do not include information not provided by the Knowledge Base.
- Addtional user prompt: {user_prompt}
- Additional user prompt: {user_prompt}
Response:"""
@ -251,7 +251,7 @@ Given the query and conversation history, list both high-level and low-level key
######################
{examples}
#############################
######################
---Real Data---
######################
Conversation History:
@ -259,42 +259,45 @@ Conversation History:
Current Query: {query}
######################
The `Output` should be human text, not unicode characters. Keep the same language as `Query`.
Output:
The `Output` should be in JSON format, with no other text before and after the JSON. Use the same language as `Current Query`.
Output:
"""
PROMPTS["keywords_extraction_examples"] = [
"""Example 1:
Query: "How does international trade influence global economic stability?"
################
Output:
{
"high_level_keywords": ["International trade", "Global economic stability", "Economic impact"],
"low_level_keywords": ["Trade agreements", "Tariffs", "Currency exchange", "Imports", "Exports"]
}
#############################""",
""",
"""Example 2:
Query: "What are the environmental consequences of deforestation on biodiversity?"
################
Output:
{
"high_level_keywords": ["Environmental consequences", "Deforestation", "Biodiversity loss"],
"low_level_keywords": ["Species extinction", "Habitat destruction", "Carbon emissions", "Rainforest", "Ecosystem"]
}
#############################""",
""",
"""Example 3:
Query: "What is the role of education in reducing poverty?"
################
Output:
{
"high_level_keywords": ["Education", "Poverty reduction", "Socioeconomic development"],
"low_level_keywords": ["School access", "Literacy rates", "Job training", "Income inequality"]
}
#############################""",
""",
]
PROMPTS["naive_rag_response"] = """---Role---

View File

@ -9,7 +9,9 @@ import logging
import logging.handlers
import os
import re
import uuid
from dataclasses import dataclass
from datetime import datetime
from functools import wraps
from hashlib import md5
from typing import Any, Protocol, Callable, TYPE_CHECKING, List
@ -19,6 +21,9 @@ from lightrag.constants import (
DEFAULT_LOG_MAX_BYTES,
DEFAULT_LOG_BACKUP_COUNT,
DEFAULT_LOG_FILENAME,
GRAPH_FIELD_SEP,
DEFAULT_MAX_TOTAL_TOKENS,
DEFAULT_MAX_FILE_PATH_LENGTH,
)
@ -55,7 +60,7 @@ def get_env_value(
# Use TYPE_CHECKING to avoid circular imports
if TYPE_CHECKING:
from lightrag.base import BaseKVStorage
from lightrag.base import BaseKVStorage, QueryParam
# use the .env that is inside the current folder
# allows to use different .env file for each lightrag instance
@ -85,8 +90,10 @@ def verbose_debug(msg: str, *args, **kwargs):
formatted_msg = msg
# Then truncate the formatted message
truncated_msg = (
formatted_msg[:100] + "..." if len(formatted_msg) > 100 else formatted_msg
formatted_msg[:150] + "..." if len(formatted_msg) > 150 else formatted_msg
)
# Remove consecutive newlines
truncated_msg = re.sub(r"\n+", "\n", truncated_msg)
logger.debug(truncated_msg, **kwargs)
@ -116,6 +123,7 @@ class LightragPathFilter(logging.Filter):
# Define paths to be filtered
self.filtered_paths = [
"/documents",
"/documents/paginated",
"/health",
"/webui/",
"/documents/pipeline_status",
@ -138,6 +146,7 @@ class LightragPathFilter(logging.Filter):
# Filter out successful GET requests to filtered paths
if (
method == "GET"
or method == "POST"
and (status == 200 or status == 304)
and path in self.filtered_paths
):
@ -232,51 +241,13 @@ class UnlimitedSemaphore:
@dataclass
class EmbeddingFunc:
embedding_dim: int
max_token_size: int
func: callable
# concurrent_limit: int = 16
max_token_size: int | None = None # deprecated keep it for compatible only
async def __call__(self, *args, **kwargs) -> np.ndarray:
return await self.func(*args, **kwargs)
def locate_json_string_body_from_string(content: str) -> str | None:
"""Locate the JSON string body from a string"""
try:
maybe_json_str = re.search(r"{.*}", content, re.DOTALL)
if maybe_json_str is not None:
maybe_json_str = maybe_json_str.group(0)
maybe_json_str = maybe_json_str.replace("\\n", "")
maybe_json_str = maybe_json_str.replace("\n", "")
maybe_json_str = maybe_json_str.replace("'", '"')
# json.loads(maybe_json_str) # don't check here, cannot validate schema after all
return maybe_json_str
except Exception:
pass
# try:
# content = (
# content.replace(kw_prompt[:-1], "")
# .replace("user", "")
# .replace("model", "")
# .strip()
# )
# maybe_json_str = "{" + content.split("{")[1].split("}")[0] + "}"
# json.loads(maybe_json_str)
return None
def convert_response_to_json(response: str) -> dict[str, Any]:
json_str = locate_json_string_body_from_string(response)
assert json_str is not None, f"Unable to parse JSON from response: {response}"
try:
data = json.loads(json_str)
return data
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON: {json_str}")
raise e from None
def compute_args_hash(*args: Any) -> str:
"""Compute a hash for the given arguments.
Args:
@ -777,39 +748,6 @@ def truncate_list_by_token_size(
return list_data
def process_combine_contexts(*context_lists):
"""
Combine multiple context lists and remove duplicate content
Args:
*context_lists: Any number of context lists
Returns:
Combined context list with duplicates removed
"""
seen_content = {}
combined_data = []
# Iterate through all input context lists
for context_list in context_lists:
if not context_list: # Skip empty lists
continue
for item in context_list:
content_dict = {
k: v for k, v in item.items() if k != "id" and k != "created_at"
}
content_key = tuple(sorted(content_dict.items()))
if content_key not in seen_content:
seen_content[content_key] = item
combined_data.append(item)
# Reassign IDs
for i, item in enumerate(combined_data):
item["id"] = str(i + 1)
return combined_data
def cosine_similarity(v1, v2):
"""Calculate cosine similarity between two vectors"""
dot_product = np.dot(v1, v2)
@ -1673,6 +1611,86 @@ def check_storage_env_vars(storage_name: str) -> None:
)
def linear_gradient_weighted_polling(
entities_or_relations: list[dict],
max_related_chunks: int,
min_related_chunks: int = 1,
) -> list[str]:
"""
Linear gradient weighted polling algorithm for text chunk selection.
This algorithm ensures that entities/relations with higher importance get more text chunks,
forming a linear decreasing allocation pattern.
Args:
entities_or_relations: List of entities or relations sorted by importance (high to low)
max_related_chunks: Expected number of text chunks for the highest importance entity/relation
min_related_chunks: Expected number of text chunks for the lowest importance entity/relation
Returns:
List of selected text chunk IDs
"""
if not entities_or_relations:
return []
n = len(entities_or_relations)
if n == 1:
# Only one entity/relation, return its first max_related_chunks text chunks
entity_chunks = entities_or_relations[0].get("sorted_chunks", [])
return entity_chunks[:max_related_chunks]
# Calculate expected text chunk count for each position (linear decrease)
expected_counts = []
for i in range(n):
# Linear interpolation: from max_related_chunks to min_related_chunks
ratio = i / (n - 1) if n > 1 else 0
expected = max_related_chunks - ratio * (
max_related_chunks - min_related_chunks
)
expected_counts.append(int(round(expected)))
# First round allocation: allocate by expected values
selected_chunks = []
used_counts = [] # Track number of chunks used by each entity
total_remaining = 0 # Accumulate remaining quotas
for i, entity_rel in enumerate(entities_or_relations):
entity_chunks = entity_rel.get("sorted_chunks", [])
expected = expected_counts[i]
# Actual allocatable count
actual = min(expected, len(entity_chunks))
selected_chunks.extend(entity_chunks[:actual])
used_counts.append(actual)
# Accumulate remaining quota
remaining = expected - actual
if remaining > 0:
total_remaining += remaining
# Second round allocation: multi-round scanning to allocate remaining quotas
for _ in range(total_remaining):
allocated = False
# Scan entities one by one, allocate one chunk when finding unused chunks
for i, entity_rel in enumerate(entities_or_relations):
entity_chunks = entity_rel.get("sorted_chunks", [])
# Check if there are still unused chunks
if used_counts[i] < len(entity_chunks):
# Allocate one chunk
selected_chunks.append(entity_chunks[used_counts[i]])
used_counts[i] += 1
allocated = True
break
# If no chunks were allocated in this round, all entities are exhausted
if not allocated:
break
return selected_chunks
class TokenTracker:
"""Track token usage for LLM calls."""
@ -1728,3 +1746,217 @@ class TokenTracker:
f"Completion tokens: {usage['completion_tokens']}, "
f"Total tokens: {usage['total_tokens']}"
)
async def apply_rerank_if_enabled(
query: str,
retrieved_docs: list[dict],
global_config: dict,
enable_rerank: bool = True,
top_n: int = None,
) -> list[dict]:
"""
Apply reranking to retrieved documents if rerank is enabled.
Args:
query: The search query
retrieved_docs: List of retrieved documents
global_config: Global configuration containing rerank settings
enable_rerank: Whether to enable reranking from query parameter
top_n: Number of top documents to return after reranking
Returns:
Reranked documents if rerank is enabled, otherwise original documents
"""
if not enable_rerank or not retrieved_docs:
return retrieved_docs
rerank_func = global_config.get("rerank_model_func")
if not rerank_func:
logger.warning(
"Rerank is enabled but no rerank model is configured. Please set up a rerank model or set enable_rerank=False in query parameters."
)
return retrieved_docs
try:
# Apply reranking - let rerank_model_func handle top_k internally
reranked_docs = await rerank_func(
query=query,
documents=retrieved_docs,
top_n=top_n,
)
if reranked_docs and len(reranked_docs) > 0:
if len(reranked_docs) > top_n:
reranked_docs = reranked_docs[:top_n]
logger.info(f"Successfully reranked: {len(retrieved_docs)} chunks")
return reranked_docs
else:
logger.warning("Rerank returned empty results, using original chunks")
return retrieved_docs
except Exception as e:
logger.error(f"Error during reranking: {e}, using original chunks")
return retrieved_docs
async def process_chunks_unified(
query: str,
unique_chunks: list[dict],
query_param: "QueryParam",
global_config: dict,
source_type: str = "mixed",
chunk_token_limit: int = None, # Add parameter for dynamic token limit
) -> list[dict]:
"""
Unified processing for text chunks: deduplication, chunk_top_k limiting, reranking, and token truncation.
Args:
query: Search query for reranking
chunks: List of text chunks to process
query_param: Query parameters containing configuration
global_config: Global configuration dictionary
source_type: Source type for logging ("vector", "entity", "relationship", "mixed")
chunk_token_limit: Dynamic token limit for chunks (if None, uses default)
Returns:
Processed and filtered list of text chunks
"""
if not unique_chunks:
return []
origin_count = len(unique_chunks)
# 1. Apply reranking if enabled and query is provided
if query_param.enable_rerank and query and unique_chunks:
rerank_top_k = query_param.chunk_top_k or len(unique_chunks)
unique_chunks = await apply_rerank_if_enabled(
query=query,
retrieved_docs=unique_chunks,
global_config=global_config,
enable_rerank=query_param.enable_rerank,
top_n=rerank_top_k,
)
# 2. Filter by minimum rerank score if reranking is enabled
if query_param.enable_rerank and unique_chunks:
min_rerank_score = global_config.get("min_rerank_score", 0.5)
if min_rerank_score > 0.0:
original_count = len(unique_chunks)
# Filter chunks with score below threshold
filtered_chunks = []
for chunk in unique_chunks:
rerank_score = chunk.get(
"rerank_score", 1.0
) # Default to 1.0 if no score
if rerank_score >= min_rerank_score:
filtered_chunks.append(chunk)
unique_chunks = filtered_chunks
filtered_count = original_count - len(unique_chunks)
if filtered_count > 0:
logger.info(
f"Rerank filtering: {len(unique_chunks)} chunks remained (min rerank score: {min_rerank_score})"
)
if not unique_chunks:
return []
# 3. Apply chunk_top_k limiting if specified
if query_param.chunk_top_k is not None and query_param.chunk_top_k > 0:
if len(unique_chunks) > query_param.chunk_top_k:
unique_chunks = unique_chunks[: query_param.chunk_top_k]
logger.debug(
f"Kept chunk_top-k: {len(unique_chunks)} chunks (deduplicated original: {origin_count})"
)
# 4. Token-based final truncation
tokenizer = global_config.get("tokenizer")
if tokenizer and unique_chunks:
# Set default chunk_token_limit if not provided
if chunk_token_limit is None:
# Get default from query_param or global_config
chunk_token_limit = getattr(
query_param,
"max_total_tokens",
global_config.get("MAX_TOTAL_TOKENS", DEFAULT_MAX_TOTAL_TOKENS),
)
original_count = len(unique_chunks)
unique_chunks = truncate_list_by_token_size(
unique_chunks,
key=lambda x: x.get("content", ""),
max_token_size=chunk_token_limit,
tokenizer=tokenizer,
)
logger.debug(
f"Token truncation: {len(unique_chunks)} chunks from {original_count} "
f"(chunk available tokens: {chunk_token_limit}, source: {source_type})"
)
return unique_chunks
def build_file_path(already_file_paths, data_list, target):
"""Build file path string with length limit and deduplication
Args:
already_file_paths: List of existing file paths
data_list: List of data items containing file_path
target: Target name for logging warnings
Returns:
str: Combined file paths separated by GRAPH_FIELD_SEP
"""
# set: deduplication
file_paths_set = {fp for fp in already_file_paths if fp}
# string: filter empty value and keep file order in already_file_paths
file_paths = GRAPH_FIELD_SEP.join(fp for fp in already_file_paths if fp)
# ignored file_paths
file_paths_ignore = ""
# add file_paths
for dp in data_list:
cur_file_path = dp.get("file_path")
# empty
if not cur_file_path:
continue
# skip duplicate item
if cur_file_path in file_paths_set:
continue
# add
file_paths_set.add(cur_file_path)
# check the length
if (
len(file_paths) + len(GRAPH_FIELD_SEP + cur_file_path)
< DEFAULT_MAX_FILE_PATH_LENGTH
):
# append
file_paths += (
GRAPH_FIELD_SEP + cur_file_path if file_paths else cur_file_path
)
else:
# ignore
file_paths_ignore += GRAPH_FIELD_SEP + cur_file_path
if file_paths_ignore:
logger.warning(
f"Length of file_path exceeds {target}, ignoring new file: {file_paths_ignore}"
)
return file_paths
def generate_track_id(prefix: str = "upload") -> str:
"""Generate a unique tracking ID with timestamp and UUID
Args:
prefix: Prefix for the track ID (e.g., 'upload', 'insert')
Returns:
str: Unique tracking ID in format: {prefix}_{timestamp}_{uuid}
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_id = str(uuid.uuid4())[:8] # Use first 8 characters of UUID
return f"{prefix}_{timestamp}_{unique_id}"

View File

@ -3,7 +3,7 @@ import ThemeProvider from '@/components/ThemeProvider'
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
import ApiKeyAlert from '@/components/ApiKeyAlert'
import StatusIndicator from '@/components/status/StatusIndicator'
import { healthCheckInterval, SiteInfo, webuiPrefix } from '@/lib/constants'
import { SiteInfo, webuiPrefix } from '@/lib/constants'
import { useBackendState, useAuthStore } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { getAuthStatus } from '@/api/lightrag'
@ -56,9 +56,6 @@ function App() {
// Health check - can be disabled
useEffect(() => {
// Only execute if health check is enabled and ApiKeyAlert is closed
if (!enableHealthCheck || apiKeyAlertOpen) return;
// Health check function
const performHealthCheck = async () => {
try {
@ -71,17 +68,27 @@ function App() {
}
};
// On first mount or when enableHealthCheck becomes true and apiKeyAlertOpen is false,
// perform an immediate health check
if (!healthCheckInitializedRef.current) {
healthCheckInitializedRef.current = true;
// Immediate health check on first load
performHealthCheck();
// Set health check function in the store
useBackendState.getState().setHealthCheckFunction(performHealthCheck);
if (!enableHealthCheck || apiKeyAlertOpen) {
useBackendState.getState().clearHealthCheckTimer();
return;
}
// Set interval for periodic execution
const interval = setInterval(performHealthCheck, healthCheckInterval * 1000);
return () => clearInterval(interval);
// On first mount or when enableHealthCheck becomes true and apiKeyAlertOpen is false,
// perform an immediate health check and start the timer
if (!healthCheckInitializedRef.current) {
healthCheckInitializedRef.current = true;
}
// Start/reset the health check timer using the store
useBackendState.getState().resetHealthCheckTimer();
// Component unmount cleanup
return () => {
useBackendState.getState().clearHealthCheckTimer();
};
}, [enableHealthCheck, apiKeyAlertOpen]);
// Version check - independent and executed only once

View File

@ -45,6 +45,15 @@ export type LightragStatus = {
enable_rerank?: boolean
rerank_model?: string | null
rerank_binding_host?: string | null
summary_language: string
force_llm_summary_on_merge: boolean
max_parallel_insert: number
max_async: number
embedding_func_max_async: number
embedding_batch_num: number
cosine_threshold: number
min_rerank_score: number
related_chunk_number: number
}
update_status?: Record<string, any>
core_version?: string
@ -134,6 +143,13 @@ export type QueryResponse = {
export type DocActionResponse = {
status: 'success' | 'partial_success' | 'failure' | 'duplicated'
message: string
track_id?: string
}
export type ScanResponse = {
status: 'scanning_started'
message: string
track_id: string
}
export type DeleteDocResponse = {
@ -151,8 +167,9 @@ export type DocStatusResponse = {
status: DocStatus
created_at: string
updated_at: string
track_id?: string
chunks_count?: number
error?: string
error_msg?: string
metadata?: Record<string, any>
file_path: string
}
@ -161,6 +178,40 @@ export type DocsStatusesResponse = {
statuses: Record<DocStatus, DocStatusResponse[]>
}
export type TrackStatusResponse = {
track_id: string
documents: DocStatusResponse[]
total_count: number
status_summary: Record<string, number>
}
export type DocumentsRequest = {
status_filter?: DocStatus | null
page: number
page_size: number
sort_field: 'created_at' | 'updated_at' | 'id' | 'file_path'
sort_direction: 'asc' | 'desc'
}
export type PaginationInfo = {
page: number
page_size: number
total_count: number
total_pages: number
has_next: boolean
has_prev: boolean
}
export type PaginatedDocsResponse = {
documents: DocStatusResponse[]
pagination: PaginationInfo
status_counts: Record<string, number>
}
export type StatusCountsResponse = {
status_counts: Record<string, number>
}
export type AuthStatusResponse = {
auth_configured: boolean
access_token?: string
@ -284,7 +335,7 @@ export const getDocuments = async (): Promise<DocsStatusesResponse> => {
return response.data
}
export const scanNewDocuments = async (): Promise<{ status: string }> => {
export const scanNewDocuments = async (): Promise<ScanResponse> => {
const response = await axiosInstance.post('/documents/scan')
return response.data
}
@ -680,3 +731,32 @@ export const checkEntityNameExists = async (entityName: string): Promise<boolean
return false
}
}
/**
* Get the processing status of documents by tracking ID
* @param trackId The tracking ID returned from upload, text, or texts endpoints
* @returns Promise with the track status response containing documents and summary
*/
export const getTrackStatus = async (trackId: string): Promise<TrackStatusResponse> => {
const response = await axiosInstance.get(`/documents/track_status/${encodeURIComponent(trackId)}`)
return response.data
}
/**
* Get documents with pagination support
* @param request The pagination request parameters
* @returns Promise with paginated documents response
*/
export const getDocumentsPaginated = async (request: DocumentsRequest): Promise<PaginatedDocsResponse> => {
const response = await axiosInstance.post('/documents/paginated', request)
return response.data
}
/**
* Get counts of documents by status
* @returns Promise with status counts response
*/
export const getDocumentStatusCounts = async (): Promise<StatusCountsResponse> => {
const response = await axiosInstance.get('/documents/status_counts')
return response.data
}

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { QueryMode, QueryRequest } from '@/api/lightrag'
// Removed unused import for Text component
import Checkbox from '@/components/ui/Checkbox'
@ -15,6 +15,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
import { useSettingsStore } from '@/stores/settings'
import { useTranslation } from 'react-i18next'
import { RotateCcw } from 'lucide-react'
export default function QuerySettings() {
const { t } = useTranslation()
@ -24,6 +25,42 @@ export default function QuerySettings() {
useSettingsStore.getState().updateQuerySettings({ [key]: value })
}, [])
// Default values for reset functionality
const defaultValues = useMemo(() => ({
mode: 'mix' as QueryMode,
response_type: 'Multiple Paragraphs',
top_k: 40,
chunk_top_k: 10,
max_entity_tokens: 10000,
max_relation_tokens: 10000,
max_total_tokens: 30000
}), [])
const handleReset = useCallback((key: keyof typeof defaultValues) => {
handleChange(key, defaultValues[key])
}, [handleChange, defaultValues])
// Reset button component
const ResetButton = ({ onClick, title }: { onClick: () => void; title: string }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onClick}
className="mr-1 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title={title}
>
<RotateCcw className="h-3 w-3 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" />
</button>
</TooltipTrigger>
<TooltipContent side="left">
<p>{title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
return (
<Card className="flex shrink-0 flex-col min-w-[220px]">
<CardHeader className="px-4 pt-4 pb-2">
@ -32,7 +69,7 @@ export default function QuerySettings() {
</CardHeader>
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
<div className="relative size-full">
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2">
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2 pr-3">
{/* Query Mode */}
<>
<TooltipProvider>
@ -47,27 +84,33 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
value={querySettings.mode}
onValueChange={(v) => handleChange('mode', v as QueryMode)}
>
<SelectTrigger
id="query_mode_select"
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0"
<div className="flex items-center gap-1">
<Select
value={querySettings.mode}
onValueChange={(v) => handleChange('mode', v as QueryMode)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
<SelectItem value="bypass">{t('retrievePanel.querySettings.queryModeOptions.bypass')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<SelectTrigger
id="query_mode_select"
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0 flex-1 text-left [&>span]:break-all [&>span]:line-clamp-1"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
<SelectItem value="bypass">{t('retrievePanel.querySettings.queryModeOptions.bypass')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<ResetButton
onClick={() => handleReset('mode')}
title="Reset to default (Mix)"
/>
</div>
</>
{/* Response Format */}
@ -84,24 +127,30 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
value={querySettings.response_type}
onValueChange={(v) => handleChange('response_type', v)}
>
<SelectTrigger
id="response_format_select"
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0"
<div className="flex items-center gap-1">
<Select
value={querySettings.response_type}
onValueChange={(v) => handleChange('response_type', v)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<SelectTrigger
id="response_format_select"
className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0 flex-1 text-left [&>span]:break-all [&>span]:line-clamp-1"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<ResetButton
onClick={() => handleReset('response_type')}
title="Reset to default (Multiple Paragraphs)"
/>
</div>
</>
{/* Top K */}
@ -118,7 +167,7 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<div className="flex items-center gap-1">
<Input
id="top_k"
type="number"
@ -135,6 +184,11 @@ export default function QuerySettings() {
}}
min={1}
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
<ResetButton
onClick={() => handleReset('top_k')}
title="Reset to default"
/>
</div>
</>
@ -153,7 +207,7 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<div className="flex items-center gap-1">
<Input
id="chunk_top_k"
type="number"
@ -170,6 +224,11 @@ export default function QuerySettings() {
}}
min={1}
placeholder={t('retrievePanel.querySettings.chunkTopKPlaceholder')}
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
<ResetButton
onClick={() => handleReset('chunk_top_k')}
title="Reset to default"
/>
</div>
</>
@ -188,7 +247,7 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<div className="flex items-center gap-1">
<Input
id="max_entity_tokens"
type="number"
@ -205,6 +264,11 @@ export default function QuerySettings() {
}}
min={1}
placeholder={t('retrievePanel.querySettings.maxEntityTokensPlaceholder')}
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
<ResetButton
onClick={() => handleReset('max_entity_tokens')}
title="Reset to default"
/>
</div>
</>
@ -223,7 +287,7 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<div className="flex items-center gap-1">
<Input
id="max_relation_tokens"
type="number"
@ -240,6 +304,11 @@ export default function QuerySettings() {
}}
min={1}
placeholder={t('retrievePanel.querySettings.maxRelationTokensPlaceholder')}
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
<ResetButton
onClick={() => handleReset('max_relation_tokens')}
title="Reset to default"
/>
</div>
</>
@ -258,7 +327,7 @@ export default function QuerySettings() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<div className="flex items-center gap-1">
<Input
id="max_total_tokens"
type="number"
@ -270,50 +339,20 @@ export default function QuerySettings() {
onBlur={(e) => {
const value = e.target.value
if (value === '' || isNaN(parseInt(value))) {
handleChange('max_total_tokens', 1000)
handleChange('max_total_tokens', 32000)
}
}}
min={1}
placeholder={t('retrievePanel.querySettings.maxTotalTokensPlaceholder')}
className="h-9 flex-1 pr-2 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
<ResetButton
onClick={() => handleReset('max_total_tokens')}
title="Reset to default"
/>
</div>
</>
{/* History Turns */}
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<label htmlFor="history_turns" className="ml-1 cursor-help">
{t('retrievePanel.querySettings.historyTurns')}
</label>
</TooltipTrigger>
<TooltipContent side="left">
<p>{t('retrievePanel.querySettings.historyTurnsTooltip')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<Input
id="history_turns"
type="number"
value={querySettings.history_turns ?? ''}
onChange={(e) => {
const value = e.target.value
handleChange('history_turns', value === '' ? '' : parseInt(value) || 0)
}}
onBlur={(e) => {
const value = e.target.value
if (value === '' || isNaN(parseInt(value))) {
handleChange('history_turns', 0)
}
}}
min={0}
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
className="h-9"
/>
</div>
</>
{/* User Prompt */}
<>

View File

@ -10,38 +10,38 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {
return (
<div className="min-w-[300px] space-y-2 text-xs">
<div className="space-y-1">
<h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
<h4 className="font-medium">{t('graphPanel.statusCard.serverInfo')}</h4>
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
<span>{t('graphPanel.statusCard.workingDirectory')}:</span>
<span className="truncate">{status.working_directory}</span>
<span>{t('graphPanel.statusCard.inputDirectory')}:</span>
<span className="truncate">{status.input_directory}</span>
<span>{t('graphPanel.statusCard.summarySettings')}:</span>
<span>{status.configuration.summary_language} / LLM summary on {status.configuration.force_llm_summary_on_merge.toString()} fragments</span>
<span>{t('graphPanel.statusCard.threshold')}:</span>
<span>cosine {status.configuration.cosine_threshold} / rerank_score {status.configuration.min_rerank_score} / max_related {status.configuration.related_chunk_number}</span>
<span>{t('graphPanel.statusCard.maxParallelInsert')}:</span>
<span>{status.configuration.max_parallel_insert}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
<span>{t('graphPanel.statusCard.llmBinding')}:</span>
<span>{status.configuration.llm_binding}</span>
<span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
<span>{status.configuration.llm_binding_host}</span>
<span>{t('graphPanel.statusCard.llmModel')}:</span>
<span>{status.configuration.llm_model}</span>
<span>{t('graphPanel.statusCard.maxTokens')}:</span>
<span>{status.configuration.max_tokens}</span>
<span>{status.configuration.llm_binding}: {status.configuration.llm_model} (#{status.configuration.max_async} Async)</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
<div className="text-foreground grid grid-cols-[160px_1fr] gap-1">
<span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
<span>{status.configuration.embedding_binding}</span>
<span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
<span>{status.configuration.embedding_binding_host}</span>
<span>{t('graphPanel.statusCard.embeddingModel')}:</span>
<span>{status.configuration.embedding_model}</span>
<span>{status.configuration.embedding_binding}: {status.configuration.embedding_model} (#{status.configuration.embedding_func_max_async} Async * {status.configuration.embedding_batch_num} batches)</span>
</div>
</div>

View File

@ -0,0 +1,259 @@
import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from './Button'
import Input from './Input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './Select'
import { cn } from '@/lib/utils'
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from 'lucide-react'
export type PaginationControlsProps = {
currentPage: number
totalPages: number
pageSize: number
totalCount: number
onPageChange: (page: number) => void
onPageSizeChange: (pageSize: number) => void
isLoading?: boolean
compact?: boolean
className?: string
}
const PAGE_SIZE_OPTIONS = [
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 50, label: '50' },
{ value: 100, label: '100' },
{ value: 200, label: '200' }
]
export default function PaginationControls({
currentPage,
totalPages,
pageSize,
totalCount,
onPageChange,
onPageSizeChange,
isLoading = false,
compact = false,
className
}: PaginationControlsProps) {
const { t } = useTranslation()
const [inputPage, setInputPage] = useState(currentPage.toString())
// Update input when currentPage changes
useEffect(() => {
setInputPage(currentPage.toString())
}, [currentPage])
// Handle page input change with debouncing
const handlePageInputChange = useCallback((value: string) => {
setInputPage(value)
}, [])
// Handle page input submit
const handlePageInputSubmit = useCallback(() => {
const pageNum = parseInt(inputPage, 10)
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
onPageChange(pageNum)
} else {
// Reset to current page if invalid
setInputPage(currentPage.toString())
}
}, [inputPage, totalPages, onPageChange, currentPage])
// Handle page input key press
const handlePageInputKeyPress = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handlePageInputSubmit()
}
}, [handlePageInputSubmit])
// Handle page size change
const handlePageSizeChange = useCallback((value: string) => {
const newPageSize = parseInt(value, 10)
if (!isNaN(newPageSize)) {
onPageSizeChange(newPageSize)
}
}, [onPageSizeChange])
// Navigation handlers
const goToFirstPage = useCallback(() => {
if (currentPage > 1 && !isLoading) {
onPageChange(1)
}
}, [currentPage, onPageChange, isLoading])
const goToPrevPage = useCallback(() => {
if (currentPage > 1 && !isLoading) {
onPageChange(currentPage - 1)
}
}, [currentPage, onPageChange, isLoading])
const goToNextPage = useCallback(() => {
if (currentPage < totalPages && !isLoading) {
onPageChange(currentPage + 1)
}
}, [currentPage, totalPages, onPageChange, isLoading])
const goToLastPage = useCallback(() => {
if (currentPage < totalPages && !isLoading) {
onPageChange(totalPages)
}
}, [currentPage, totalPages, onPageChange, isLoading])
if (totalPages <= 1) {
return null
}
if (compact) {
return (
<div className={cn('flex items-center gap-2', className)}>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={goToPrevPage}
disabled={currentPage <= 1 || isLoading}
className="h-8 w-8 p-0"
>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<div className="flex items-center gap-1">
<Input
type="text"
value={inputPage}
onChange={(e) => handlePageInputChange(e.target.value)}
onBlur={handlePageInputSubmit}
onKeyPress={handlePageInputKeyPress}
disabled={isLoading}
className="h-8 w-12 text-center text-sm"
/>
<span className="text-sm text-gray-500">/ {totalPages}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={goToNextPage}
disabled={currentPage >= totalPages || isLoading}
className="h-8 w-8 p-0"
>
<ChevronRightIcon className="h-4 w-4" />
</Button>
</div>
<Select
value={pageSize.toString()}
onValueChange={handlePageSizeChange}
disabled={isLoading}
>
<SelectTrigger className="h-8 w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
return (
<div className={cn('flex items-center justify-between gap-4', className)}>
<div className="text-sm text-gray-500">
{t('pagination.showing', {
start: Math.min((currentPage - 1) * pageSize + 1, totalCount),
end: Math.min(currentPage * pageSize, totalCount),
total: totalCount
})}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={goToFirstPage}
disabled={currentPage <= 1 || isLoading}
className="h-8 w-8 p-0"
tooltip={t('pagination.firstPage')}
>
<ChevronsLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={goToPrevPage}
disabled={currentPage <= 1 || isLoading}
className="h-8 w-8 p-0"
tooltip={t('pagination.prevPage')}
>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<div className="flex items-center gap-1">
<span className="text-sm">{t('pagination.page')}</span>
<Input
type="text"
value={inputPage}
onChange={(e) => handlePageInputChange(e.target.value)}
onBlur={handlePageInputSubmit}
onKeyPress={handlePageInputKeyPress}
disabled={isLoading}
className="h-8 w-16 text-center text-sm"
/>
<span className="text-sm">/ {totalPages}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={goToNextPage}
disabled={currentPage >= totalPages || isLoading}
className="h-8 w-8 p-0"
tooltip={t('pagination.nextPage')}
>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={goToLastPage}
disabled={currentPage >= totalPages || isLoading}
className="h-8 w-8 p-0"
tooltip={t('pagination.lastPage')}
>
<ChevronsRightIcon className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">{t('pagination.pageSize')}</span>
<Select
value={pageSize.toString()}
onValueChange={handlePageSizeChange}
disabled={isLoading}
>
<SelectTrigger className="h-8 w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
)
}

View File

@ -18,13 +18,22 @@ import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog'
import DeselectDocumentsDialog from '@/components/documents/DeselectDocumentsDialog'
import PaginationControls from '@/components/ui/PaginationControls'
import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
import {
scanNewDocuments,
getDocumentsPaginated,
DocsStatusesResponse,
DocStatus,
DocStatusResponse,
DocumentsRequest,
PaginationInfo
} from '@/api/lightrag'
import { errorMessage } from '@/lib/utils'
import { toast } from 'sonner'
import { useBackendState } from '@/stores/state'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, FilterIcon } from 'lucide-react'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon } from 'lucide-react'
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
type StatusFilter = DocStatus | 'all';
@ -136,7 +145,7 @@ const pulseStyle = `
`;
// Type definitions for sort field and direction
type SortField = 'created_at' | 'updated_at' | 'id';
type SortField = 'created_at' | 'updated_at' | 'id' | 'file_path';
type SortDirection = 'asc' | 'desc';
export default function DocumentManager() {
@ -164,10 +173,28 @@ export default function DocumentManager() {
const { t, i18n } = useTranslation()
const health = useBackendState.use.health()
const pipelineBusy = useBackendState.use.pipelineBusy()
// Legacy state for backward compatibility
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
const currentTab = useSettingsStore.use.currentTab()
const showFileName = useSettingsStore.use.showFileName()
const setShowFileName = useSettingsStore.use.setShowFileName()
const documentsPageSize = useSettingsStore.use.documentsPageSize()
const setDocumentsPageSize = useSettingsStore.use.setDocumentsPageSize()
// New pagination state
const [, setCurrentPageDocs] = useState<DocStatusResponse[]>([])
const [pagination, setPagination] = useState<PaginationInfo>({
page: 1,
page_size: documentsPageSize,
total_count: 0,
total_pages: 0,
has_next: false,
has_prev: false
})
const [statusCounts, setStatusCounts] = useState<Record<string, number>>({ all: 0 })
const [isRefreshing, setIsRefreshing] = useState(false)
// Sort state
const [sortField, setSortField] = useState<SortField>('updated_at')
@ -176,6 +203,15 @@ export default function DocumentManager() {
// State for document status filter
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
// State to store page number for each status filter
const [pageByStatus, setPageByStatus] = useState<Record<StatusFilter, number>>({
all: 1,
processed: 1,
processing: 1,
pending: 1,
failed: 1,
});
// State for document selection
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([])
const isSelectionMode = selectedDocIds.length > 0
@ -198,15 +234,30 @@ export default function DocumentManager() {
// Handle sort column click
const handleSort = (field: SortField) => {
if (sortField === field) {
// Toggle sort direction if clicking the same field
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
} else {
// Set new sort field with default desc direction
setSortField(field)
setSortDirection('desc')
let actualField = field;
// When clicking the first column, determine the actual sort field based on showFileName
if (field === 'id') {
actualField = showFileName ? 'file_path' : 'id';
}
}
const newDirection = (sortField === actualField && sortDirection === 'desc') ? 'asc' : 'desc';
setSortField(actualField);
setSortDirection(newDirection);
// Reset page to 1 when sorting changes
setPagination(prev => ({ ...prev, page: 1 }));
// Reset all status filters' page memory since sorting affects all
setPageByStatus({
all: 1,
processed: 1,
processing: 1,
pending: 1,
failed: 1,
});
};
// Sort documents based on current sort field and direction
const sortDocuments = useCallback((documents: DocStatusResponse[]) => {
@ -373,74 +424,84 @@ export default function DocumentManager() {
};
}, [docs]);
const fetchDocuments = useCallback(async () => {
// New paginated data fetching function
const fetchPaginatedDocuments = useCallback(async (
page: number,
pageSize: number,
statusFilter: StatusFilter
) => {
try {
// Check if component is still mounted before starting the request
if (!isMountedRef.current) return;
const docs = await getDocuments();
setIsRefreshing(true);
// Prepare request parameters
const request: DocumentsRequest = {
status_filter: statusFilter === 'all' ? null : statusFilter,
page,
page_size: pageSize,
sort_field: sortField,
sort_direction: sortDirection
};
const response = await getDocumentsPaginated(request);
// Check again if component is still mounted after the request completes
if (!isMountedRef.current) return;
// Only update state if component is still mounted
if (isMountedRef.current) {
// Update docs state
if (docs && docs.statuses) {
const numDocuments = Object.values(docs.statuses).reduce(
(acc, status) => acc + status.length,
0
)
if (numDocuments > 0) {
setDocs(docs)
} else {
setDocs(null)
}
} else {
setDocs(null)
// Update pagination state
setPagination(response.pagination);
setCurrentPageDocs(response.documents);
setStatusCounts(response.status_counts);
// Update legacy docs state for backward compatibility
const legacyDocs: DocsStatusesResponse = {
statuses: {
processed: response.documents.filter(doc => doc.status === 'processed'),
processing: response.documents.filter(doc => doc.status === 'processing'),
pending: response.documents.filter(doc => doc.status === 'pending'),
failed: response.documents.filter(doc => doc.status === 'failed')
}
};
if (response.pagination.total_count > 0) {
setDocs(legacyDocs);
} else {
setDocs(null);
}
} catch (err) {
// Only show error if component is still mounted
if (isMountedRef.current) {
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
}
} finally {
if (isMountedRef.current) {
setIsRefreshing(false);
}
}
}, [setDocs, t])
}, [sortField, sortDirection, t]);
// Fetch documents when the tab becomes visible
useEffect(() => {
if (currentTab === 'documents') {
fetchDocuments()
// Legacy fetchDocuments function for backward compatibility
const fetchDocuments = useCallback(async () => {
await fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
}, [fetchPaginatedDocuments, pagination.page, pagination.page_size, statusFilter]);
// Add refs to track previous pipelineBusy state and current interval
const prevPipelineBusyRef = useRef<boolean | undefined>(undefined);
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Function to clear current polling interval
const clearPollingInterval = useCallback(() => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
}, [currentTab, fetchDocuments])
}, []);
const scanDocuments = useCallback(async () => {
try {
// Check if component is still mounted before starting the request
if (!isMountedRef.current) return;
// Function to start polling with given interval
const startPollingInterval = useCallback((intervalMs: number) => {
clearPollingInterval();
const { status } = await scanNewDocuments();
// Check again if component is still mounted after the request completes
if (!isMountedRef.current) return;
toast.message(status);
} catch (err) {
// Only show error if component is still mounted
if (isMountedRef.current) {
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }));
}
}
}, [t])
// Set up polling when the documents tab is active and health is good
useEffect(() => {
if (currentTab !== 'documents' || !health) {
return
}
const interval = setInterval(async () => {
pollingIntervalRef.current = setInterval(async () => {
try {
// Only perform fetch if component is still mounted
if (isMountedRef.current) {
@ -452,12 +513,145 @@ export default function DocumentManager() {
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
}
}
}, 5000)
}, intervalMs);
}, [fetchDocuments, t, clearPollingInterval]);
const scanDocuments = useCallback(async () => {
try {
// Check if component is still mounted before starting the request
if (!isMountedRef.current) return;
const { status, message, track_id: _track_id } = await scanNewDocuments(); // eslint-disable-line @typescript-eslint/no-unused-vars
// Check again if component is still mounted after the request completes
if (!isMountedRef.current) return;
// Note: _track_id is available for future use (e.g., progress tracking)
toast.message(message || status);
// Reset health check timer with 1 second delay to avoid race condition
useBackendState.getState().resetHealthCheckTimerDelayed(1000);
// Schedule a health check 2 seconds after successful scan
startPollingInterval(2000);
} catch (err) {
// Only show error if component is still mounted
if (isMountedRef.current) {
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }));
}
}
}, [t, startPollingInterval])
// Handle page size change - update state and save to store
const handlePageSizeChange = useCallback((newPageSize: number) => {
if (newPageSize === pagination.page_size) return;
// Save the new page size to the store
setDocumentsPageSize(newPageSize);
// Reset all status filters to page 1 when page size changes
setPageByStatus({
all: 1,
processed: 1,
processing: 1,
pending: 1,
failed: 1,
});
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
}, [pagination.page_size, setDocumentsPageSize]);
// Handle manual refresh with pagination reset logic
const handleManualRefresh = useCallback(async () => {
try {
setIsRefreshing(true);
// Fetch documents from the first page
const request: DocumentsRequest = {
status_filter: statusFilter === 'all' ? null : statusFilter,
page: 1,
page_size: pagination.page_size,
sort_field: sortField,
sort_direction: sortDirection
};
const response = await getDocumentsPaginated(request);
if (!isMountedRef.current) return;
// Check if total count is less than current page size and page size is not already 10
if (response.pagination.total_count < pagination.page_size && pagination.page_size !== 10) {
// Reset page size to 10 which will trigger a new fetch
handlePageSizeChange(10);
} else {
// Update pagination state
setPagination(response.pagination);
setCurrentPageDocs(response.documents);
setStatusCounts(response.status_counts);
// Update legacy docs state for backward compatibility
const legacyDocs: DocsStatusesResponse = {
statuses: {
processed: response.documents.filter(doc => doc.status === 'processed'),
processing: response.documents.filter(doc => doc.status === 'processing'),
pending: response.documents.filter(doc => doc.status === 'pending'),
failed: response.documents.filter(doc => doc.status === 'failed')
}
};
if (response.pagination.total_count > 0) {
setDocs(legacyDocs);
} else {
setDocs(null);
}
}
} catch (err) {
if (isMountedRef.current) {
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }));
}
} finally {
if (isMountedRef.current) {
setIsRefreshing(false);
}
}
}, [statusFilter, pagination.page_size, sortField, sortDirection, handlePageSizeChange, t]);
// Monitor pipelineBusy changes and trigger immediate refresh with timer reset
useEffect(() => {
// Skip the first render when prevPipelineBusyRef is undefined
if (prevPipelineBusyRef.current !== undefined && prevPipelineBusyRef.current !== pipelineBusy) {
// pipelineBusy state has changed, trigger immediate refresh
if (currentTab === 'documents' && health && isMountedRef.current) {
handleManualRefresh();
// Reset polling timer after manual refresh
const hasActiveDocuments = (statusCounts.processing || 0) > 0 || (statusCounts.pending || 0) > 0;
const pollingInterval = hasActiveDocuments ? 5000 : 30000;
startPollingInterval(pollingInterval);
}
}
// Update the previous state
prevPipelineBusyRef.current = pipelineBusy;
}, [pipelineBusy, currentTab, health, handleManualRefresh, statusCounts.processing, statusCounts.pending, startPollingInterval]);
// Set up intelligent polling with dynamic interval based on document status
useEffect(() => {
if (currentTab !== 'documents' || !health) {
clearPollingInterval();
return
}
// Determine polling interval based on document status
const hasActiveDocuments = (statusCounts.processing || 0) > 0 || (statusCounts.pending || 0) > 0;
const pollingInterval = hasActiveDocuments ? 5000 : 30000; // 5s if active, 30s if idle
startPollingInterval(pollingInterval);
return () => {
clearInterval(interval)
clearPollingInterval();
}
}, [health, fetchDocuments, t, currentTab])
}, [health, t, currentTab, statusCounts, startPollingInterval, clearPollingInterval])
// Monitor docs changes to check status counts and trigger health check if needed
useEffect(() => {
@ -485,16 +679,97 @@ export default function DocumentManager() {
prevStatusCounts.current = newStatusCounts
}, [docs]);
// Handle page change - only update state
const handlePageChange = useCallback((newPage: number) => {
if (newPage === pagination.page) return;
// Save the new page for current status filter
setPageByStatus(prev => ({ ...prev, [statusFilter]: newPage }));
setPagination(prev => ({ ...prev, page: newPage }));
}, [pagination.page, statusFilter]);
// Handle status filter change - only update state
const handleStatusFilterChange = useCallback((newStatusFilter: StatusFilter) => {
if (newStatusFilter === statusFilter) return;
// Save current page for the current status filter
setPageByStatus(prev => ({ ...prev, [statusFilter]: pagination.page }));
// Get the saved page for the new status filter
const newPage = pageByStatus[newStatusFilter];
// Update status filter and restore the saved page
setStatusFilter(newStatusFilter);
setPagination(prev => ({ ...prev, page: newPage }));
}, [statusFilter, pagination.page, pageByStatus]);
// Handle documents deleted callback
const handleDocumentsDeleted = useCallback(async () => {
setSelectedDocIds([])
await fetchDocuments()
}, [fetchDocuments])
// Add dependency on sort state to re-render when sort changes
// Reset health check timer with 1 second delay to avoid race condition
useBackendState.getState().resetHealthCheckTimerDelayed(1000)
// Schedule a health check 2 seconds after successful clear
startPollingInterval(2000)
}, [startPollingInterval])
// Handle documents cleared callback with proper interval reset
const handleDocumentsCleared = useCallback(async () => {
// Clear current polling interval
clearPollingInterval();
// Reset status counts to ensure proper state
setStatusCounts({
all: 0,
processed: 0,
processing: 0,
pending: 0,
failed: 0
});
// Perform one immediate refresh to confirm clear operation
if (isMountedRef.current) {
try {
await fetchDocuments();
} catch (err) {
console.error('Error fetching documents after clear:', err);
}
}
// Set appropriate polling interval based on current state
// Since documents are cleared, use idle interval (30 seconds)
if (currentTab === 'documents' && health && isMountedRef.current) {
startPollingInterval(30000); // 30 seconds for idle state
}
}, [clearPollingInterval, setStatusCounts, fetchDocuments, currentTab, health, startPollingInterval])
// Handle showFileName change - switch sort field if currently sorting by first column
useEffect(() => {
// This effect ensures the component re-renders when sort state changes
}, [sortField, sortDirection]);
// Only switch if currently sorting by the first column (id or file_path)
if (sortField === 'id' || sortField === 'file_path') {
const newSortField = showFileName ? 'file_path' : 'id';
if (sortField !== newSortField) {
setSortField(newSortField);
}
}
}, [showFileName, sortField]);
// Central effect to handle all data fetching
useEffect(() => {
if (currentTab === 'documents') {
fetchPaginatedDocuments(pagination.page, pagination.page_size, statusFilter);
}
}, [
currentTab,
pagination.page,
pagination.page_size,
statusFilter,
sortField,
sortDirection,
fetchPaginatedDocuments
]);
return (
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
@ -502,7 +777,7 @@ export default function DocumentManager() {
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex flex-col min-h-0 overflow-auto">
<div className="flex gap-2 mb-2">
<div className="flex justify-between items-center gap-2 mb-2">
<div className="flex gap-2">
<Button
variant="outline"
@ -526,27 +801,43 @@ export default function DocumentManager() {
<ActivityIcon /> {t('documentPanel.documentManager.pipelineStatusButton')}
</Button>
</div>
<div className="flex-1" />
{isSelectionMode && (
<DeleteDocumentsDialog
selectedDocIds={selectedDocIds}
totalCompletedCount={documentCounts.processed || 0}
onDocumentsDeleted={handleDocumentsDeleted}
{/* Pagination Controls in the middle */}
{pagination.total_pages > 1 && (
<PaginationControls
currentPage={pagination.page}
totalPages={pagination.total_pages}
pageSize={pagination.page_size}
totalCount={pagination.total_count}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
isLoading={isRefreshing}
compact={true}
/>
)}
{isSelectionMode ? (
<DeselectDocumentsDialog
selectedCount={selectedDocIds.length}
onDeselect={handleDeselectAll}
<div className="flex gap-2">
{isSelectionMode && (
<DeleteDocumentsDialog
selectedDocIds={selectedDocIds}
totalCompletedCount={documentCounts.processed || 0}
onDocumentsDeleted={handleDocumentsDeleted}
/>
)}
{isSelectionMode ? (
<DeselectDocumentsDialog
selectedCount={selectedDocIds.length}
onDeselect={handleDeselectAll}
/>
) : (
<ClearDocumentsDialog onDocumentsCleared={handleDocumentsCleared} />
)}
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
<PipelineStatusDialog
open={showPipelineStatus}
onOpenChange={setShowPipelineStatus}
/>
) : (
<ClearDocumentsDialog onDocumentsCleared={fetchDocuments} />
)}
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
<PipelineStatusDialog
open={showPipelineStatus}
onOpenChange={setShowPipelineStatus}
/>
</div>
</div>
<Card className="flex-1 flex flex-col border rounded-md min-h-0 mb-2">
@ -554,63 +845,77 @@ export default function DocumentManager() {
<div className="flex justify-between items-center">
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
<div className="flex items-center gap-2">
<FilterIcon className="h-4 w-4" />
<div className="flex gap-1" dir={i18n.dir()}>
<Button
size="sm"
variant={statusFilter === 'all' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('all')}
onClick={() => handleStatusFilterChange('all')}
disabled={isRefreshing}
className={cn(
statusFilter === 'all' && 'bg-gray-100 dark:bg-gray-900 font-medium border border-gray-400 dark:border-gray-500 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.all')} ({documentCounts.all})
{t('documentPanel.documentManager.status.all')} ({statusCounts.all || documentCounts.all})
</Button>
<Button
size="sm"
variant={statusFilter === 'processed' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('processed')}
onClick={() => handleStatusFilterChange('processed')}
disabled={isRefreshing}
className={cn(
documentCounts.processed > 0 ? 'text-green-600' : 'text-gray-500',
(statusCounts.PROCESSED || statusCounts.processed || documentCounts.processed) > 0 ? 'text-green-600' : 'text-gray-500',
statusFilter === 'processed' && 'bg-green-100 dark:bg-green-900/30 font-medium border border-green-400 dark:border-green-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.completed')} ({documentCounts.processed || 0})
{t('documentPanel.documentManager.status.completed')} ({statusCounts.PROCESSED || statusCounts.processed || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'processing' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('processing')}
onClick={() => handleStatusFilterChange('processing')}
disabled={isRefreshing}
className={cn(
documentCounts.processing > 0 ? 'text-blue-600' : 'text-gray-500',
(statusCounts.PROCESSING || statusCounts.processing || documentCounts.processing) > 0 ? 'text-blue-600' : 'text-gray-500',
statusFilter === 'processing' && 'bg-blue-100 dark:bg-blue-900/30 font-medium border border-blue-400 dark:border-blue-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.processing')} ({documentCounts.processing || 0})
{t('documentPanel.documentManager.status.processing')} ({statusCounts.PROCESSING || statusCounts.processing || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('pending')}
onClick={() => handleStatusFilterChange('pending')}
disabled={isRefreshing}
className={cn(
documentCounts.pending > 0 ? 'text-yellow-600' : 'text-gray-500',
(statusCounts.PENDING || statusCounts.pending || documentCounts.pending) > 0 ? 'text-yellow-600' : 'text-gray-500',
statusFilter === 'pending' && 'bg-yellow-100 dark:bg-yellow-900/30 font-medium border border-yellow-400 dark:border-yellow-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.pending')} ({documentCounts.pending || 0})
{t('documentPanel.documentManager.status.pending')} ({statusCounts.PENDING || statusCounts.pending || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('failed')}
onClick={() => handleStatusFilterChange('failed')}
disabled={isRefreshing}
className={cn(
documentCounts.failed > 0 ? 'text-red-600' : 'text-gray-500',
(statusCounts.FAILED || statusCounts.failed || documentCounts.failed) > 0 ? 'text-red-600' : 'text-gray-500',
statusFilter === 'failed' && 'bg-red-100 dark:bg-red-900/30 font-medium border border-red-400 dark:border-red-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.failed')} ({documentCounts.failed || 0})
{t('documentPanel.documentManager.status.failed')} ({statusCounts.FAILED || statusCounts.failed || 0})
</Button>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleManualRefresh}
disabled={isRefreshing}
side="bottom"
tooltip={t('documentPanel.documentManager.refreshTooltip')}
>
<RotateCcwIcon className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<label
@ -656,8 +961,11 @@ export default function DocumentManager() {
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
>
<div className="flex items-center">
{t('documentPanel.documentManager.columns.id')}
{sortField === 'id' && (
{showFileName
? t('documentPanel.documentManager.columns.fileName')
: t('documentPanel.documentManager.columns.id')
}
{((sortField === 'id' && !showFileName) || (sortField === 'file_path' && showFileName)) && (
<span className="ml-1">
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
</span>
@ -749,8 +1057,8 @@ export default function DocumentManager() {
{doc.status === 'failed' && (
<span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>
)}
{doc.error && (
<span className="ml-2 text-red-500" title={doc.error}>
{doc.error_msg && (
<span className="ml-2 text-red-500" title={doc.error_msg}>
</span>
)}

View File

@ -5,7 +5,7 @@ import { Settings as SigmaSettings } from 'sigma/settings'
import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search'
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
import { NodeBorderProgram } from '@sigma/node-border'
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
import { EdgeCurvedArrowProgram, createEdgeCurveProgram } from '@sigma/edge-curve'
import FocusOnNode from '@/components/graph/FocusOnNode'
import LayoutsControl from '@/components/graph/LayoutsControl'
@ -36,7 +36,7 @@ const defaultSigmaSettings: Partial<SigmaSettings> = {
edgeProgramClasses: {
arrow: EdgeArrowProgram,
curvedArrow: EdgeCurvedArrowProgram,
curvedNoArrow: EdgeCurveProgram
curvedNoArrow: createEdgeCurveProgram()
},
nodeProgramClasses: {
default: NodeBorderProgram,

Some files were not shown because too many files have changed in this diff Show More