diff --git a/create_index_script.py b/create_index_script.py index dc7b894e..baf2d717 100644 --- a/create_index_script.py +++ b/create_index_script.py @@ -34,10 +34,16 @@ class IndexCreator: - """Interactive index creation utility.""" + """Interactive index creation utility for LocalGPT RAG system.""" def __init__(self, config_path: Optional[str] = None): - """Initialize the index creator with optional custom configuration.""" + """ + Initialize the index creator with optional custom configuration. + + Args: + config_path: Optional path to custom configuration file. If not provided, + uses default configuration. + """ self.db = ChatDatabase() self.config = self._load_config(config_path) @@ -56,7 +62,15 @@ def __init__(self, config_path: Optional[str] = None): ) def _load_config(self, config_path: Optional[str] = None) -> dict: - """Load configuration from file or use default.""" + """ + Load configuration from file or use default. + + Args: + config_path: Optional path to configuration file. + + Returns: + Dictionary containing configuration settings. + """ if config_path and os.path.exists(config_path): try: with open(config_path, 'r') as f: @@ -68,14 +82,31 @@ def _load_config(self, config_path: Optional[str] = None) -> dict: return PIPELINE_CONFIGS.get("default", {}) def get_user_input(self, prompt: str, default: str = "") -> str: - """Get user input with optional default value.""" + """ + Get user input with optional default value. + + Args: + prompt: The prompt message to display to the user. + default: Default value to use if user provides no input. + + Returns: + User input string or default value if no input provided. + """ if default: user_input = input(f"{prompt} [{default}]: ").strip() return user_input if user_input else default return input(f"{prompt}: ").strip() def select_documents(self) -> List[str]: - """Interactive document selection.""" + """ + Interactive document selection interface. + + Provides options to add single documents, entire directories, + and manage the selected document list. + + Returns: + List of absolute paths to selected documents. + """ print("\n๐Ÿ“ Document Selection") print("=" * 50) @@ -141,7 +172,15 @@ def select_documents(self) -> List[str]: return documents def configure_processing(self) -> dict: - """Interactive processing configuration.""" + """ + Interactive processing configuration interface. + + Allows users to configure document processing parameters including + chunk size, overlap, enrichment options, and model selection. + + Returns: + Dictionary containing processing configuration settings. + """ print("\nโš™๏ธ Processing Configuration") print("=" * 50) @@ -175,7 +214,12 @@ def configure_processing(self) -> dict: } def create_index_interactive(self) -> None: - """Run the interactive index creation process.""" + """ + Run the interactive index creation process. + + Guides the user through the complete index creation workflow including + naming, document selection, configuration, and processing. + """ print("๐Ÿš€ LocalGPT Index Creation Tool") print("=" * 50) @@ -237,7 +281,12 @@ def create_index_interactive(self) -> None: traceback.print_exc() def test_index(self, index_id: str) -> None: - """Test the created index with a sample query.""" + """ + Test the created index with a sample query. + + Args: + index_id: The ID of the index to test. + """ try: print("\n๐Ÿงช Testing Index") print("=" * 50) @@ -258,7 +307,15 @@ def test_index(self, index_id: str) -> None: print(f"โŒ Error testing index: {e}") def batch_create_from_config(self, config_file: str) -> None: - """Create index from batch configuration file.""" + """ + Create index from batch configuration file. + + Processes a JSON configuration file containing index settings and + document paths for automated index creation. + + Args: + config_file: Path to the batch configuration JSON file. + """ try: with open(config_file, 'r') as f: batch_config = json.load(f) @@ -312,7 +369,12 @@ def batch_create_from_config(self, config_file: str) -> None: def create_sample_batch_config(): - """Create a sample batch configuration file.""" + """ + Create a sample batch configuration file. + + Generates a JSON configuration file with example settings that can be + used as a template for batch index creation. + """ sample_config = { "index_name": "Sample Batch Index", "index_description": "Example batch index configuration", @@ -340,7 +402,12 @@ def create_sample_batch_config(): def main(): - """Main entry point for the script.""" + """ + Main entry point for the script. + + Parses command line arguments and executes the appropriate index creation + workflow based on the provided options. + """ parser = argparse.ArgumentParser(description="LocalGPT Index Creation Tool") parser.add_argument("--batch", help="Batch configuration file", type=str) parser.add_argument("--config", help="Custom pipeline configuration file", type=str) @@ -369,4 +436,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() \ No newline at end of file diff --git a/rag_system/api_server.py b/rag_system/api_server.py index de148361..36a240e4 100644 --- a/rag_system/api_server.py +++ b/rag_system/api_server.py @@ -40,7 +40,11 @@ # -------------- Helper ---------------- def _apply_index_embedding_model(idx_ids): - """Ensure retrieval pipeline uses the embedding model stored with the first index.""" + """Ensure retrieval pipeline uses the embedding model stored with the first index. + + Args: + idx_ids (list): List of index IDs to check for embedding model metadata. + """ debug_info = f"๐Ÿ”ง _apply_index_embedding_model called with idx_ids: {idx_ids}\n" if not idx_ids: @@ -69,7 +73,14 @@ def _apply_index_embedding_model(idx_ids): f.write(debug_info) def _get_table_name_for_session(session_id): - """Get the correct vector table name for a session by looking up its linked indexes.""" + """Get the correct vector table name for a session by looking up its linked indexes. + + Args: + session_id (str): The session ID to look up indexes for. + + Returns: + str or None: The vector table name for the session, or None if not found. + """ logger = logging.getLogger(__name__) if not session_id: @@ -113,6 +124,12 @@ def _get_table_name_for_session(session_id): return default_table class AdvancedRagApiHandler(http.server.BaseHTTPRequestHandler): + """HTTP request handler for the RAG API server. + + Handles POST requests for chat interactions, streaming responses, and document indexing. + Also handles GET requests for retrieving available models. + """ + def do_OPTIONS(self): """Handle CORS preflight requests for frontend integration.""" self.send_response(200) @@ -122,7 +139,13 @@ def do_OPTIONS(self): self.end_headers() def do_POST(self): - """Handle POST requests for chat and indexing.""" + """Handle POST requests for chat and indexing endpoints. + + Routes requests to appropriate handlers based on the URL path: + - /chat: Regular chat interactions + - /chat/stream: Streaming chat responses + - /index: Document indexing + """ parsed_path = urlparse(self.path) if parsed_path.path == '/chat': @@ -135,6 +158,11 @@ def do_POST(self): self.send_json_response({"error": "Not Found"}, status_code=404) def do_GET(self): + """Handle GET requests for retrieving information. + + Routes requests to appropriate handlers based on the URL path: + - /models: List available models + """ parsed_path = urlparse(self.path) if parsed_path.path == '/models': @@ -143,7 +171,16 @@ def do_GET(self): self.send_json_response({"error": "Not Found"}, status_code=404) def handle_chat(self): - """Handles a chat query by calling the agentic RAG pipeline.""" + """Handle a chat query by calling the agentic RAG pipeline. + + Processes JSON request body containing query, session_id, and various configuration flags. + Updates session title for first messages and stores user/assistant messages in database. + Returns JSON response with the generated answer and metadata. + + Raises: + json.JSONDecodeError: If the request body contains invalid JSON. + Exception: For various server errors during processing. + """ try: content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) @@ -302,7 +339,17 @@ def handle_chat(self): self.send_json_response({"error": f"Server error: {str(e)}"}, status_code=500) def handle_chat_stream(self): - """Stream internal phases and final answer using SSE (text/event-stream).""" + """Stream internal phases and final answer using SSE (text/event-stream). + + Similar to handle_chat but streams responses using Server-Sent Events. + Emits events for different processing phases and the final result. + Handles client disconnections gracefully. + + Raises: + json.JSONDecodeError: If the request body contains invalid JSON. + BrokenPipeError: If the client disconnects during streaming. + Exception: For various server errors during processing. + """ try: content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) @@ -385,7 +432,15 @@ def handle_chat_stream(self): self.end_headers() def emit(event_type: str, payload): - """Send a single SSE event.""" + """Send a single SSE event. + + Args: + event_type (str): The type of event to emit. + payload: The data payload to send with the event. + + Raises: + BrokenPipeError: If the client has disconnected. + """ try: data_str = json.dumps({"type": event_type, "data": payload}) self.wfile.write(f"data: {data_str}\n\n".encode('utf-8')) @@ -499,7 +554,16 @@ def emit(event_type: str, payload): self.send_json_response({"error": f"Server error: {str(e)}"}, status_code=500) def handle_index(self): - """Triggers the document indexing pipeline for specific files.""" + """Trigger the document indexing pipeline for specific files. + + Processes JSON request body containing file paths and indexing configuration. + Creates temporary pipeline instances with custom configurations for each indexing job. + Updates index metadata with embedding model information. + + Raises: + json.JSONDecodeError: If the request body contains invalid JSON. + Exception: For various server errors during indexing. + """ try: content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) @@ -690,7 +754,15 @@ def handle_index(self): self.send_json_response({"error": f"Failed to start indexing: {str(e)}"}, status_code=500) def handle_models(self): - """Return a list of locally installed Ollama models and supported HuggingFace models, grouped by capability.""" + """Return a list of locally installed Ollama models and supported HuggingFace models, grouped by capability. + + Queries the Ollama API for available models and classifies them as generation or embedding models. + Also includes a predefined list of supported HuggingFace embedding models. + Returns JSON response with models grouped by capability. + + Raises: + Exception: If there are errors querying the Ollama API or processing model data. + """ try: generation_models = [] embedding_models = [] @@ -732,7 +804,12 @@ def handle_models(self): self.send_json_response({"error": f"Could not list models: {e}"}, status_code=500) def send_json_response(self, data, status_code=200): - """Utility to send a JSON response with CORS headers.""" + """Send a JSON response with CORS headers. + + Args: + data: The data to serialize as JSON and send in the response body. + status_code (int): HTTP status code to send. Defaults to 200. + """ self.send_response(status_code) self.send_header('Content-Type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') @@ -741,12 +818,17 @@ def send_json_response(self, data, status_code=200): self.wfile.write(response.encode('utf-8')) def start_server(port=8001): - """Starts the API server.""" + """Start the API server on the specified port. + + Args: + port (int): Port number to bind the server to. Defaults to 8001. + """ # Use a reusable TCP server to avoid "address in use" errors on restart class ReusableTCPServer(socketserver.TCPServer): + """TCP server that allows address reuse to prevent binding errors on restart.""" allow_reuse_address = True - with ReusableTCPServer(("", port), AdvancedRagApiHandler) as httpd: + with ReusableTCPServer("", port, AdvancedRagApiHandler) as httpd: print(f"๐Ÿš€ Starting Advanced RAG API server on port {port}") print(f"๐Ÿ’ฌ Chat endpoint: http://localhost:{port}/chat") print(f"โœจ Indexing endpoint: http://localhost:{port}/index") @@ -754,4 +836,4 @@ class ReusableTCPServer(socketserver.TCPServer): if __name__ == "__main__": # To run this server: python -m rag_system.api_server - start_server() \ No newline at end of file + start_server() \ No newline at end of file diff --git a/rag_system/indexing/contextualizer.py b/rag_system/indexing/contextualizer.py index 714c65d3..dd480bec 100644 --- a/rag_system/indexing/contextualizer.py +++ b/rag_system/indexing/contextualizer.py @@ -29,15 +29,40 @@ class ContextualEnricher: """ Enriches chunks with a prepended summary of their surrounding context using Ollama, while preserving the original text. + + This class provides functionality to enhance text chunks by adding contextual summaries + that help situate each chunk within its broader document context. The enrichment process + uses an LLM to generate concise summaries based on surrounding text. """ def __init__(self, llm_client: OllamaClient, llm_model: str, batch_size: int = 10): + """ + Initialize the ContextualEnricher with an Ollama client and model configuration. + + Args: + llm_client (OllamaClient): The Ollama client instance for LLM interactions + llm_model (str): The name of the Ollama model to use for generating summaries + batch_size (int, optional): Number of chunks to process in each batch. Defaults to 10. + """ self.llm_client = llm_client self.llm_model = llm_model self.batch_size = batch_size logger.info(f"Initialized ContextualEnricher with Ollama model '{self.llm_model}' (batch_size={batch_size}).") def _generate_summary(self, local_context_text: str, chunk_text: str) -> str: - """Generates a contextual summary using a structured, multi-part prompt.""" + """ + Generates a contextual summary using a structured, multi-part prompt. + + This method creates a summary that situates the given chunk within its local context + by sending a structured prompt to the LLM and processing the response to remove + any chain-of-thought markers or unwanted formatting. + + Args: + local_context_text (str): The surrounding context text for the chunk + chunk_text (str): The specific chunk content to be contextualized + + Returns: + str: A clean contextual summary, or empty string if generation fails + """ # Combine the templates to form the final content for the HumanMessage equivalent human_prompt_content = ( f"{LOCAL_CONTEXT_PROMPT_TEMPLATE.format(local_context_text=local_context_text)}\n\n" @@ -80,6 +105,20 @@ def _generate_summary(self, local_context_text: str, chunk_text: str) -> str: return "" # Gracefully fail by returning no summary def enrich_chunks(self, chunks: List[Dict[str, Any]], window_size: int = 1) -> List[Dict[str, Any]]: + """ + Enrich chunks with contextual summaries using batch processing for improved performance. + + This method processes chunks in batches to add contextual summaries that help situate + each chunk within its document context. The original text is preserved while a summary + is prepended to provide additional context. + + Args: + chunks (List[Dict[str, Any]]): List of chunk dictionaries containing 'text' and 'metadata' keys + window_size (int, optional): Number of surrounding chunks to include in context window. Defaults to 1. + + Returns: + List[Dict[str, Any]]: List of enriched chunks with contextual summaries prepended to text + """ if not chunks: return [] @@ -96,7 +135,15 @@ def enrich_chunks(self, chunks: List[Dict[str, Any]], window_size: int = 1) -> L batch_processor = BatchProcessor(batch_size=self.batch_size) def process_chunk_batch(chunk_indices): - """Process a batch of chunk indices for contextual enrichment""" + """ + Process a batch of chunk indices for contextual enrichment. + + Args: + chunk_indices: List of chunk indices to process in this batch + + Returns: + List[Dict[str, Any]]: List of enriched chunks for the given indices + """ batch_results = [] for i in chunk_indices: chunk = chunks[i] @@ -144,7 +191,19 @@ def process_chunk_batch(chunk_indices): return enriched_chunks def enrich_chunks_sequential(self, chunks: List[Dict[str, Any]], window_size: int = 1) -> List[Dict[str, Any]]: - """Sequential enrichment method (legacy) - kept for comparison""" + """ + Sequential enrichment method for processing chunks one by one. + + This is a legacy method kept for comparison purposes. It processes chunks sequentially + without batch processing, which may be slower but uses less memory for large datasets. + + Args: + chunks (List[Dict[str, Any]]): List of chunk dictionaries containing 'text' and 'metadata' keys + window_size (int, optional): Number of surrounding chunks to include in context window. Defaults to 1. + + Returns: + List[Dict[str, Any]]: List of enriched chunks with contextual summaries prepended to text + """ if not chunks: return [] diff --git a/rag_system/ingestion/chunking.py b/rag_system/ingestion/chunking.py index 6e55ff0a..bcd3ece9 100644 --- a/rag_system/ingestion/chunking.py +++ b/rag_system/ingestion/chunking.py @@ -9,6 +9,14 @@ class MarkdownRecursiveChunker: """ def __init__(self, max_chunk_size: int = 1500, min_chunk_size: int = 200, tokenizer_model: str = "Qwen/Qwen3-Embedding-0.6B"): + """ + Initialize the MarkdownRecursiveChunker with specified parameters. + + Args: + max_chunk_size: Maximum number of tokens allowed per chunk. + min_chunk_size: Minimum number of tokens required per chunk. + tokenizer_model: Name or path of the tokenizer model to use for token counting. + """ self.max_chunk_size = max_chunk_size self.min_chunk_size = min_chunk_size self.split_priority = ["\n## ", "\n### ", "\n#### ", "```", "\n\n"] @@ -27,13 +35,32 @@ def __init__(self, max_chunk_size: int = 1500, min_chunk_size: int = 200, tokeni self.tokenizer = None def _token_len(self, text: str) -> int: - """Get token count for text using the tokenizer.""" + """ + Get token count for text using the tokenizer. + + Args: + text: The text to count tokens for. + + Returns: + The number of tokens in the text. If tokenizer is unavailable, + returns character count divided by 4 as an approximation. + """ if self.tokenizer is not None: return len(self.tokenizer.tokenize(text)) else: return max(1, len(text) // 4) def _split_text(self, text: str, separators: List[str]) -> List[str]: + """ + Split text recursively using a list of separators in priority order. + + Args: + text: The text to split. + separators: List of separator strings to use for splitting, in order of priority. + + Returns: + List of text chunks after splitting and processing. + """ final_chunks = [] chunks_to_process = [text] @@ -126,6 +153,20 @@ def chunk(self, text: str, document_id: str, document_metadata: Optional[Dict[st return final_chunks def create_contextual_window(all_chunks: List[Dict[str, Any]], chunk_index: int, window_size: int = 1) -> str: + """ + Create a contextual window around a specific chunk by combining surrounding chunks. + + Args: + all_chunks: List of all chunk dictionaries containing 'text' keys. + chunk_index: Index of the target chunk to create context around. + window_size: Number of chunks to include before and after the target chunk. + + Returns: + Combined text from the contextual window chunks. + + Raises: + ValueError: If chunk_index is out of bounds for the all_chunks list. + """ if not (0 <= chunk_index < len(all_chunks)): raise ValueError("chunk_index is out of bounds.") start = max(0, chunk_index - window_size) diff --git a/rag_system/utils/batch_processor.py b/rag_system/utils/batch_processor.py index 25d1eaeb..e02c5307 100644 --- a/rag_system/utils/batch_processor.py +++ b/rag_system/utils/batch_processor.py @@ -10,7 +10,14 @@ @contextmanager def timer(operation_name: str): - """Context manager to time operations""" + """Context manager to time operations and log the duration. + + Args: + operation_name: Name of the operation being timed + + Yields: + None + """ start = time.time() try: yield @@ -22,6 +29,12 @@ class ProgressTracker: """Tracks progress and performance metrics for batch operations""" def __init__(self, total_items: int, operation_name: str = "Processing"): + """Initialize the progress tracker. + + Args: + total_items: Total number of items to be processed + operation_name: Name of the operation for logging purposes + """ self.total_items = total_items self.operation_name = operation_name self.processed_items = 0 @@ -31,7 +44,12 @@ def __init__(self, total_items: int, operation_name: str = "Processing"): self.report_interval = 10 # Report every 10 seconds def update(self, items_processed: int, errors: int = 0): - """Update progress with number of items processed""" + """Update progress with number of items processed. + + Args: + items_processed: Number of items that were processed in this update + errors: Number of errors encountered in this update + """ self.processed_items += items_processed self.errors_encountered += errors @@ -41,7 +59,7 @@ def update(self, items_processed: int, errors: int = 0): self.last_report_time = current_time def _report_progress(self): - """Report current progress""" + """Report current progress including percentage, rate, and ETA.""" elapsed = time.time() - self.start_time if elapsed > 0: rate = self.processed_items / elapsed @@ -57,7 +75,7 @@ def _report_progress(self): ) def finish(self): - """Report final statistics""" + """Report final statistics including total time, rate, and error count.""" elapsed = time.time() - self.start_time rate = self.processed_items / elapsed if elapsed > 0 else 0 @@ -70,6 +88,12 @@ class BatchProcessor: """Generic batch processor with progress tracking and error handling""" def __init__(self, batch_size: int = 50, enable_gc: bool = True): + """Initialize the batch processor. + + Args: + batch_size: Number of items to process in each batch + enable_gc: Whether to enable periodic garbage collection + """ self.batch_size = batch_size self.enable_gc = enable_gc @@ -128,7 +152,14 @@ def process_in_batches( return results def batch_iterator(self, items: List[Any]) -> Iterator[List[Any]]: - """Generate batches as an iterator for memory-efficient processing""" + """Generate batches as an iterator for memory-efficient processing. + + Args: + items: List of items to split into batches + + Yields: + List[Any]: Batch of items with size up to batch_size + """ for i in range(0, len(items), self.batch_size): yield items[i:i + self.batch_size] @@ -136,6 +167,11 @@ class StreamingProcessor: """Process items one at a time with minimal memory usage""" def __init__(self, enable_gc_interval: int = 100): + """Initialize the streaming processor. + + Args: + enable_gc_interval: Interval for triggering garbage collection (0 to disable) + """ self.enable_gc_interval = enable_gc_interval def process_streaming( @@ -187,7 +223,14 @@ def process_streaming( # Utility functions for common batch operations def batch_chunks_by_document(chunks: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: - """Group chunks by document_id for document-level batch processing""" + """Group chunks by document_id for document-level batch processing. + + Args: + chunks: List of chunk dictionaries containing metadata with document_id + + Returns: + Dictionary mapping document_id to list of chunks for that document + """ document_batches = {} for chunk in chunks: doc_id = chunk.get('metadata', {}).get('document_id', 'unknown') @@ -197,7 +240,14 @@ def batch_chunks_by_document(chunks: List[Dict[str, Any]]) -> Dict[str, List[Dic return document_batches def estimate_memory_usage(chunks: List[Dict[str, Any]]) -> float: - """Estimate memory usage of chunks in MB""" + """Estimate memory usage of chunks in MB. + + Args: + chunks: List of chunk dictionaries to estimate memory usage for + + Returns: + Estimated memory usage in megabytes + """ if not chunks: return 0.0 @@ -209,6 +259,14 @@ def estimate_memory_usage(chunks: List[Dict[str, Any]]) -> float: if __name__ == '__main__': # Test the batch processor def dummy_process_func(batch): + """Simulate processing a batch of items with a delay. + + Args: + batch: List of items to process + + Returns: + List of processed items with 'processed_' prefix + """ time.sleep(0.1) # Simulate processing time return [f"processed_{item}" for item in batch] @@ -220,4 +278,4 @@ def dummy_process_func(batch): "Test Processing" ) - print(f"Processed {len(results)} items") \ No newline at end of file + print(f"Processed {len(results)} items") \ No newline at end of file diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index c00f07f9..e38ac287 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -5,6 +5,14 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" +/** + * Avatar root component that provides the container for avatar content. + * Renders a circular container with default styling that can be customized. + * + * @param className - Additional CSS classes to apply to the avatar container + * @param props - All other props are forwarded to the underlying Radix Avatar Root component + * @returns JSX element representing the avatar container + */ function Avatar({ className, ...props @@ -21,6 +29,14 @@ function Avatar({ ) } +/** + * Avatar image component for displaying the main avatar image. + * Automatically handles image loading states and fallback behavior. + * + * @param className - Additional CSS classes to apply to the image element + * @param props - All other props are forwarded to the underlying Radix Avatar Image component + * @returns JSX element representing the avatar image + */ function AvatarImage({ className, ...props @@ -34,6 +50,14 @@ function AvatarImage({ ) } +/** + * Avatar fallback component that displays when the main image fails to load. + * Typically contains initials, icons, or other placeholder content. + * + * @param className - Additional CSS classes to apply to the fallback element + * @param props - All other props are forwarded to the underlying Radix Avatar Fallback component + * @returns JSX element representing the avatar fallback content + */ function AvatarFallback({ className, ...props diff --git a/src/components/ui/chat-input.tsx b/src/components/ui/chat-input.tsx index a6ff6770..d1d74d97 100644 --- a/src/components/ui/chat-input.tsx +++ b/src/components/ui/chat-input.tsx @@ -6,6 +6,17 @@ import { ArrowUp, Settings as SettingsIcon, Plus, X, FileText } from "lucide-rea import { Button } from "@/components/ui/button" import { AttachedFile } from "@/lib/types" +/** + * Props for the ChatInput component + * @interface ChatInputProps + * @property {function} onSendMessage - Callback function called when a message is sent, receives message text and optional attached files + * @property {boolean} [disabled] - Whether the input is disabled + * @property {string} [placeholder] - Placeholder text for the textarea + * @property {string} [className] - Additional CSS classes to apply to the container + * @property {function} [onOpenSettings] - Callback function called when settings button is clicked + * @property {function} [onAddIndex] - Callback function called when add index button is clicked + * @property {React.ReactNode} [leftExtras] - Additional React elements to render in the left section of the action row + */ interface ChatInputProps { onSendMessage: (message: string, attachedFiles?: AttachedFile[]) => Promise disabled?: boolean @@ -16,6 +27,11 @@ interface ChatInputProps { leftExtras?: React.ReactNode } +/** + * A chat input component with file attachment support and auto-resizing textarea + * @param {ChatInputProps} props - The component props + * @returns {JSX.Element} The rendered chat input component + */ export function ChatInput({ onSendMessage, disabled = false, @@ -31,6 +47,11 @@ export function ChatInput({ const textareaRef = useRef(null) const fileInputRef = useRef(null) + /** + * Handles form submission by sending the message and attached files + * @param {React.FormEvent} e - The form event + * @returns {Promise} + */ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if ((!message.trim() && attachedFiles.length === 0) || disabled || isLoading) return @@ -53,6 +74,10 @@ export function ChatInput({ } } + /** + * Handles keyboard events in the textarea, submitting on Enter key press + * @param {React.KeyboardEvent} e - The keyboard event + */ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() @@ -60,6 +85,10 @@ export function ChatInput({ } } + /** + * Handles textarea input changes and auto-resizes the textarea height + * @param {React.ChangeEvent} e - The input change event + */ const handleInput = (e: React.ChangeEvent) => { setMessage(e.target.value) @@ -71,10 +100,17 @@ export function ChatInput({ } } + /** + * Triggers the hidden file input to open the file selection dialog + */ const handleFileAttach = () => { fileInputRef.current?.click() } + /** + * Handles file selection from the file input, filtering for supported file types + * @param {React.ChangeEvent} e - The file input change event + */ const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files if (!files) return @@ -122,10 +158,19 @@ export function ChatInput({ } } + /** + * Removes a file from the attached files list by its ID + * @param {string} fileId - The unique identifier of the file to remove + */ const removeFile = (fileId: string) => { setAttachedFiles(prev => prev.filter(f => f.id !== fileId)) } + /** + * Formats a file size in bytes to a human-readable string + * @param {number} bytes - The file size in bytes + * @returns {string} The formatted file size string + */ const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes' const k = 1024 @@ -211,4 +256,4 @@ export function ChatInput({ ) -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/ui/chat-settings-modal.tsx b/src/components/ui/chat-settings-modal.tsx index 3e9d9f69..08e8943a 100644 --- a/src/components/ui/chat-settings-modal.tsx +++ b/src/components/ui/chat-settings-modal.tsx @@ -3,6 +3,9 @@ import { GlassToggle } from '@/components/ui/GlassToggle'; import { InfoTooltip } from '@/components/ui/InfoTooltip'; +/** + * Configuration option for a toggle/boolean setting + */ export interface ToggleOption { type: 'toggle'; label: string; @@ -10,6 +13,9 @@ export interface ToggleOption { setter: (v: boolean) => void; } +/** + * Configuration option for a slider/range setting + */ export interface SliderOption { type: 'slider'; label: string; @@ -21,6 +27,9 @@ export interface SliderOption { unit?: string; } +/** + * Configuration option for a dropdown/select setting + */ export interface DropdownOption { type: 'dropdown'; label: string; @@ -29,13 +38,22 @@ export interface DropdownOption { options: { value: string; label: string }[]; } +/** + * Union type representing any type of setting option + */ export type SettingOption = ToggleOption | SliderOption | DropdownOption; +/** + * Props for the ChatSettingsModal component + */ interface Props { options: SettingOption[]; onClose: () => void; } +/** + * Help text mapping for various setting options + */ const optionHelp: Record = { 'Query decomposition':'Breaks a complex question into sub-queries to improve recall (adds latency).', 'Compose sub-answers':'Merges answers from decomposed sub-queries into a single response.', @@ -52,7 +70,17 @@ const optionHelp: Record = { 'Reranker top chunks':'Limit how many chunks are re-ranked to speed up processing.' }; +/** + * Modal component for configuring chat settings with various option types + * @param props - Component props containing options array and close handler + * @returns JSX element representing the settings modal + */ export function ChatSettingsModal({ options, onClose }: Props) { + /** + * Renders a setting option based on its type (toggle, slider, or dropdown) + * @param opt - The setting option to render + * @returns JSX element for the specific option type or null if type is unknown + */ const renderOption = (opt: SettingOption) => { switch (opt.type) { case 'toggle': @@ -117,6 +145,9 @@ export function ChatSettingsModal({ options, onClose }: Props) { } }; + /** + * Labels for toggle options that should be displayed in a grid layout + */ const gridToggleLabels: string[] = [ 'Query decomposition', 'Compose sub-answers', @@ -126,8 +157,16 @@ export function ChatSettingsModal({ options, onClose }: Props) { 'Stream phases', ]; + /** + * Labels for retrieval settings that should be displayed in a grid layout + */ const retrievalGridLabels = ['LLM model','Search type']; + /** + * Maps internal option labels to user-friendly display names + * @param label - The internal label to transform + * @returns The display-friendly version of the label + */ const displayName = (label: string) => { if (label === 'Always search documents') return 'RAG (no-triage)'; if (label === 'LLM model') return 'LLM'; @@ -136,6 +175,11 @@ export function ChatSettingsModal({ options, onClose }: Props) { return label; }; + /** + * Finds and renders a setting option by its label with display name override + * @param label - The label of the option to find and render + * @returns JSX element for the option or null if not found + */ const renderOptionOrdered = (label: string) => { const opt = options.find(o => o.label === label); if (!opt) return null; @@ -164,7 +208,11 @@ export function ChatSettingsModal({ options, onClose }: Props) {

Retrieval Settings

{/* LLM + Search type grid */} - {(() => { + {/** + * Creates a grid layout for retrieval settings by mapping labels to options + * @returns JSX element containing the grid of retrieval options + */ + (() => { const arr: SettingOption[] = retrievalGridLabels .map(lbl => { const opt = options.find(o=>o.label===lbl); @@ -201,4 +249,4 @@ export function ChatSettingsModal({ options, onClose }: Props) {
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/ui/conversation-page.tsx b/src/components/ui/conversation-page.tsx index 191f1554..93196543 100644 --- a/src/components/ui/conversation-page.tsx +++ b/src/components/ui/conversation-page.tsx @@ -12,13 +12,23 @@ import { cn } from "@/lib/utils" import Markdown from "@/components/Markdown" import { normalizeWhitespace } from "@/utils/textNormalization" +/** + * Props for the ConversationPage component + */ interface ConversationPageProps { + /** Array of chat messages to display */ messages: ChatMessage[] + /** Whether the conversation is currently loading */ isLoading?: boolean + /** Additional CSS classes to apply */ className?: string + /** Callback function triggered when an action is performed on a message */ onAction?: (action: string, messageId: string, messageContent: string) => void } +/** + * Configuration for action icons displayed on messages + */ const actionIcons = [ { icon: Copy, type: "Copy", action: "copy" }, { icon: ThumbsUp, type: "Like", action: "like" }, @@ -28,7 +38,12 @@ const actionIcons = [ { icon: MoreHorizontal, type: "More", action: "more" }, ] -// Citation block toggle component +/** + * Renders a collapsible citation block with document preview + * @param doc - The document object containing text and metadata + * @param idx - The index of the citation for numbering + * @returns JSX element representing a citation + */ function Citation({doc, idx}: {doc:any, idx:number}){ const [open,setOpen]=React.useState(false); const preview = (doc.text||'').replace(/\s+/g,' ').trim().slice(0,160) + ((doc.text||'').length>160?'โ€ฆ':''); @@ -39,7 +54,11 @@ function Citation({doc, idx}: {doc:any, idx:number}){ ); } -// NEW: Expandable list of citations per assistant message +/** + * Renders an expandable list of citations for assistant messages + * @param docs - Array of document objects to display as citations + * @returns JSX element containing the citations block or null if no citations + */ function CitationsBlock({docs}:{docs:any[]}){ const scored = docs.filter(d => d.rerank_score || d.score || d._distance) scored.sort((a, b) => (b.rerank_score ?? b.score ?? 1/b._distance) - (a.rerank_score ?? a.score ?? 1/a._distance)) @@ -67,6 +86,11 @@ function CitationsBlock({docs}:{docs:any[]}){ ); } +/** + * Renders an icon based on the step status + * @param status - The current status of the step + * @returns JSX element with the appropriate icon or null + */ function StepIcon({ status }: { status: 'pending' | 'active' | 'done' | 'error' }) { switch (status) { case 'pending': @@ -82,6 +106,9 @@ function StepIcon({ status }: { status: 'pending' | 'active' | 'done' | 'error' } } +/** + * CSS classes for step status borders + */ const statusBorder: Record = { pending: 'border-neutral-800', active: 'border-blue-400 animate-pulse', @@ -89,7 +116,11 @@ const statusBorder: Record = { error: 'border-red-400' } -// Component to handle tokens and render them in a collapsible block +/** + * Processes text containing tags and renders them in a collapsible block + * @param text - The text content that may contain thinking blocks + * @returns JSX element with thinking blocks separated from main content + */ function ThinkingText({ text }: { text: string }) { const regex = /([\s\S]*?)<\/think>/g; const thinkSegments: string[] = []; @@ -117,6 +148,11 @@ function ThinkingText({ text }: { text: string }) { ); } +/** + * Renders structured message content with steps and progress indicators + * @param content - Array of step objects or object containing steps array + * @returns JSX element displaying the structured message with timeline + */ function StructuredMessageBlock({ content }: { content: Array> | { steps: any[] } }) { const steps: any[] = Array.isArray(content) ? content : (content as any).steps; // Determine if sub-query answers are present @@ -197,6 +233,14 @@ function StructuredMessageBlock({ content }: { content: Array scrollContainer.removeEventListener('scroll', handleScroll) }, []) + /** + * Scrolls the conversation to the bottom using multiple fallback methods + */ const scrollToBottom = () => { // Try multiple methods to ensure scrolling works if (messagesEndRef.current) { @@ -250,6 +297,12 @@ export function ConversationPage({ }, 100) } + /** + * Handles action button clicks on messages + * @param action - The type of action performed + * @param messageId - The ID of the message + * @param messageContent - The content of the message + */ const handleAction = (action: string, messageId: string, messageContent: string) => { if (onAction) { // For structured messages, we'll just join the text parts for copy/paste @@ -414,4 +467,4 @@ export function ConversationPage({ )} ) -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index dcf2bd17..eb3727d8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,10 @@ const API_BASE_URL = 'http://localhost:8000'; -// ๐Ÿ†• Simple UUID generator for client-side message IDs +/** + * Generates a UUID string for client-side message identification. + * Uses the native crypto.randomUUID() when available, otherwise falls back to a custom implementation. + * @returns {string} A UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + */ export const generateUUID = () => { if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) { return window.crypto.randomUUID(); @@ -13,50 +17,94 @@ export const generateUUID = () => { }); }; +/** + * Represents a step in a multi-step process or workflow. + */ export interface Step { + /** Unique identifier for the step */ key: string; + /** Human-readable label for the step */ label: string; + /** Current status of the step */ status: 'pending' | 'active' | 'done'; + /** Additional details or data associated with the step */ details: any; } +/** + * Represents a chat message in the conversation. + */ export interface ChatMessage { + /** Unique identifier for the message */ id: string; + /** Content of the message, can be text, structured data, or steps */ content: string | Array> | { steps: Step[] }; + /** Who sent the message */ sender: 'user' | 'assistant'; + /** ISO timestamp when the message was created */ timestamp: string; + /** Whether the message is currently being processed */ isLoading?: boolean; + /** Additional metadata associated with the message */ metadata?: Record; } +/** + * Represents a chat session containing multiple messages. + */ export interface ChatSession { + /** Unique identifier for the session */ id: string; + /** Title or name of the session */ title: string; + /** ISO timestamp when the session was created */ created_at: string; + /** ISO timestamp when the session was last updated */ updated_at: string; + /** The AI model used in this session */ model_used: string; + /** Total number of messages in the session */ message_count: number; } +/** + * Request payload for sending a chat message. + */ export interface ChatRequest { + /** The message content to send */ message: string; + /** Optional AI model to use for the response */ model?: string; + /** Optional conversation history for context */ conversation_history?: Array<{ role: 'user' | 'assistant'; content: string; }>; } +/** + * Response from the chat API after sending a message. + */ export interface ChatResponse { + /** The AI's response message */ response: string; + /** The model that generated the response */ model: string; + /** Total number of messages in the conversation */ message_count: number; } +/** + * Response from the health check endpoint. + */ export interface HealthResponse { + /** Overall status of the service */ status: string; + /** Whether the Ollama service is running */ ollama_running: boolean; + /** List of available AI models */ available_models: string[]; + /** Optional database statistics */ database_stats?: { total_sessions: number; total_messages: number; @@ -64,24 +112,50 @@ export interface HealthResponse { }; } +/** + * Response containing available AI models. + */ export interface ModelsResponse { + /** Models available for text generation */ generation_models: string[]; + /** Models available for text embedding */ embedding_models: string[]; } +/** + * Response containing a list of chat sessions. + */ export interface SessionResponse { + /** Array of chat sessions */ sessions: ChatSession[]; + /** Total number of sessions */ total: number; } +/** + * Response from sending a message within a specific session. + */ export interface SessionChatResponse { + /** The AI's response message */ response: string; + /** Updated session information */ session: ChatSession; + /** ID of the user's message */ user_message_id: string; + /** ID of the AI's response message */ ai_message_id: string; } +/** + * API client for interacting with the chat service backend. + * Provides methods for health checks, messaging, session management, and file operations. + */ class ChatAPI { + /** + * Checks the health status of the chat service. + * @returns {Promise} Health status information including available models and database stats + * @throws {Error} When the health check request fails + */ async checkHealth(): Promise { try { const response = await fetch(`${API_BASE_URL}/health`); @@ -95,6 +169,12 @@ class ChatAPI { } } + /** + * Sends a chat message and receives a response. + * @param {ChatRequest} request - The chat request containing message and optional parameters + * @returns {Promise} The AI's response to the message + * @throws {Error} When the chat request fails + */ async sendMessage(request: ChatRequest): Promise { try { const response = await fetch(`${API_BASE_URL}/chat`, { @@ -121,7 +201,12 @@ class ChatAPI { } } - // Convert ChatMessage array to conversation history format + /** + * Converts an array of ChatMessage objects to conversation history format. + * Filters out non-string content and empty messages. + * @param {ChatMessage[]} messages - Array of chat messages to convert + * @returns {Array<{role: 'user' | 'assistant', content: string}>} Formatted conversation history + */ messagesToHistory(messages: ChatMessage[]): Array<{ role: 'user' | 'assistant'; content: string }> { return messages .filter(msg => typeof msg.content === 'string' && msg.content.trim()) @@ -131,7 +216,11 @@ class ChatAPI { })); } - // Session Management + /** + * Retrieves all chat sessions for the user. + * @returns {Promise} List of sessions with metadata + * @throws {Error} When the request fails + */ async getSessions(): Promise { try { const response = await fetch(`${API_BASE_URL}/sessions`); @@ -145,6 +234,13 @@ class ChatAPI { } } + /** + * Creates a new chat session. + * @param {string} [title='New Chat'] - Title for the new session + * @param {string} [model='llama3.2:latest'] - AI model to use for the session + * @returns {Promise} The created session object + * @throws {Error} When session creation fails + */ async createSession(title: string = 'New Chat', model: string = 'llama3.2:latest'): Promise { try { const response = await fetch(`${API_BASE_URL}/sessions`, { @@ -167,6 +263,12 @@ class ChatAPI { } } + /** + * Retrieves a specific session and its messages. + * @param {string} sessionId - ID of the session to retrieve + * @returns {Promise<{session: ChatSession, messages: ChatMessage[]}>} Session data and message history + * @throws {Error} When the session cannot be retrieved + */ async getSession(sessionId: string): Promise<{ session: ChatSession; messages: ChatMessage[] }> { try { const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}`); @@ -180,6 +282,27 @@ class ChatAPI { } } + /** + * Sends a message within a specific session with advanced options. + * @param {string} sessionId - ID of the session to send the message to + * @param {string} message - The message content to send + * @param {Object} [opts={}] - Optional parameters for message processing + * @param {string} [opts.model] - AI model to use for this message + * @param {boolean} [opts.composeSubAnswers] - Whether to compose sub-answers + * @param {boolean} [opts.decompose] - Whether to decompose the query + * @param {boolean} [opts.aiRerank] - Whether to use AI reranking + * @param {boolean} [opts.contextExpand] - Whether to expand context + * @param {boolean} [opts.verify] - Whether to verify the response + * @param {number} [opts.retrievalK] - Number of documents to retrieve + * @param {number} [opts.contextWindowSize] - Size of the context window + * @param {number} [opts.rerankerTopK] - Top K for reranking + * @param {string} [opts.searchType] - Type of search to perform + * @param {number} [opts.denseWeight] - Weight for dense retrieval + * @param {boolean} [opts.forceRag] - Whether to force RAG usage + * @param {boolean} [opts.provencePrune] - Whether to prune provenance + * @returns {Promise} Response with source documents + * @throws {Error} When the message sending fails + */ async sendSessionMessage( sessionId: string, message: string, @@ -237,6 +360,12 @@ class ChatAPI { } } + /** + * Deletes a chat session permanently. + * @param {string} sessionId - ID of the session to delete + * @returns {Promise<{message: string, deleted_session_id: string}>} Confirmation of deletion + * @throws {Error} When the deletion fails + */ async deleteSession(sessionId: string): Promise<{ message: string; deleted_session_id: string }> { try { const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}`, { @@ -255,6 +384,13 @@ class ChatAPI { } } + /** + * Renames an existing chat session. + * @param {string} sessionId - ID of the session to rename + * @param {string} newTitle - New title for the session + * @returns {Promise<{message: string, session: ChatSession}>} Updated session information + * @throws {Error} When the rename operation fails + */ async renameSession(sessionId: string, newTitle: string): Promise<{ message: string; session: ChatSession }> { try { const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}/rename`, { @@ -277,6 +413,11 @@ class ChatAPI { } } + /** + * Removes empty sessions that have no messages. + * @returns {Promise<{message: string, cleanup_count: number}>} Number of sessions cleaned up + * @throws {Error} When the cleanup operation fails + */ async cleanupEmptySessions(): Promise<{ message: string; cleanup_count: number }> { try { const response = await fetch(`${API_BASE_URL}/sessions/cleanup`); @@ -293,6 +434,13 @@ class ChatAPI { } } + /** + * Uploads files to a specific session. + * @param {string} sessionId - ID of the session to upload files to + * @param {File[]} files - Array of files to upload + * @returns {Promise<{message: string, uploaded_files: {filename: string, stored_path: string}[]}>} Upload results + * @throws {Error} When the upload fails + */ async uploadFiles(sessionId: string, files: File[]): Promise<{ message: string; uploaded_files: {filename: string, stored_path: string}[]; @@ -319,6 +467,12 @@ class ChatAPI { } } + /** + * Triggers indexing of documents for a session to enable search and retrieval. + * @param {string} sessionId - ID of the session to index documents for + * @returns {Promise<{message: string}>} Indexing status message + * @throws {Error} When the indexing fails + */ async indexDocuments(sessionId: string): Promise<{ message: string }> { try { const response = await fetch(`${API_BASE_URL}/sessions/${sessionId}/index`, { @@ -339,7 +493,14 @@ class ChatAPI { } } - // Legacy upload function - can be removed if no longer needed + /** + * Legacy method for uploading PDF files with detailed processing information. + * @deprecated Use uploadFiles instead + * @param {string} sessionId - ID of the session to upload PDFs to + * @param {File[]} files - Array of PDF files to upload + * @returns {Promise<{message: string, uploaded_files: any[], processing_results: any[], session_documents: any[], total_session_documents: number}>} Detailed upload results + * @throws {Error} When the upload fails or files exceed size limits + */ async uploadPDFs(sessionId: string, files: File[]): Promise<{ message: string; uploaded_files: any[]; @@ -392,7 +553,11 @@ class ChatAPI { } } - // Convert database message format to ChatMessage format + /** + * Converts a database message record to a ChatMessage object. + * @param {Record} dbMessage - Raw message data from database + * @returns {ChatMessage} Formatted chat message object + */ convertDbMessage(dbMessage: Record): ChatMessage { return { id: dbMessage.id as string, @@ -403,7 +568,13 @@ class ChatAPI { }; } - // Create a new ChatMessage with UUID (for loading states) + /** + * Creates a new ChatMessage object with a generated UUID. + * @param {string} content - Message content + * @param {'user' | 'assistant'} sender - Who is sending the message + * @param {boolean} [isLoading=false] - Whether the message is in a loading state + * @returns {ChatMessage} New chat message object + */ createMessage( content: string, sender: 'user' | 'assistant', @@ -418,7 +589,11 @@ class ChatAPI { }; } - // ---------------- Models ---------------- + /** + * Retrieves the list of available AI models. + * @returns {Promise} Available generation and embedding models + * @throws {Error} When the models list cannot be fetched + */ async getModels(): Promise { const resp = await fetch(`${API_BASE_URL}/models`); if (!resp.ok) { @@ -427,6 +602,12 @@ class ChatAPI { return resp.json(); } + /** + * Retrieves documents associated with a specific session. + * @param {string} sessionId - ID of the session to get documents for + * @returns {Promise<{files: string[], file_count: number, session: ChatSession}>} Session documents information + * @throws {Error} When the documents cannot be retrieved + */ async getSessionDocuments(sessionId: string): Promise<{ files: string[]; file_count: number; session: ChatSession }> { const resp = await fetch(`${API_BASE_URL}/sessions/${sessionId}/documents`); if (!resp.ok) { @@ -435,8 +616,14 @@ class ChatAPI { return resp.json(); } - // ---------- Index endpoints ---------- - + /** + * Creates a new document index for organizing and searching documents. + * @param {string} name - Name of the index + * @param {string} [description] - Optional description of the index + * @param {Record} [metadata={}] - Additional metadata for the index + * @returns {Promise<{index_id: string}>} ID of the created index + * @throws {Error} When index creation fails + */ async createIndex(name: string, description?: string, metadata: Record = {}): Promise<{ index_id: string }> { const resp = await fetch(`${API_BASE_URL}/indexes`, { method: 'POST', @@ -450,6 +637,13 @@ class ChatAPI { return resp.json(); } + /** + * Uploads files to a specific document index. + * @param {string} indexId - ID of the index to upload files to + * @param {File[]} files - Array of files to upload + * @returns {Promise<{message: string, uploaded_files: any[]}>} Upload results + * @throws {Error} When the upload to index fails + */ async uploadFilesToIndex(indexId: string, files: File[]): Promise<{ message: string; uploaded_files: any[] }> { const fd = new FormData(); files.forEach((f) => fd.append('files', f, f.name)); @@ -461,6 +655,25 @@ class ChatAPI { return resp.json(); } + /** + * Builds a document index with specified processing options. + * @param {string} indexId - ID of the index to build + * @param {Object} [opts={}] - Build configuration options + * @param {boolean} [opts.latechunk] - Whether to use late chunking + * @param {boolean} [opts.doclingChunk] - Whether to use docling chunking + * @param {number} [opts.chunkSize] - Size of text chunks + * @param {number} [opts.chunkOverlap] - Overlap between chunks + * @param {string} [opts.retrievalMode] - Retrieval mode to use + * @param {number} [opts.windowSize] - Context window size + * @param {boolean} [opts.enableEnrich] - Whether to enable enrichment + * @param {string} [opts.embeddingModel] - Model for embeddings + * @param {string} [opts.enrichModel] - Model for enrichment + * @param {string} [opts.overviewModel] - Model for overviews + * @param {number} [opts.batchSizeEmbed] - Batch size for embedding + * @param {number} [opts.batchSizeEnrich] - Batch size for enrichment + * @returns {Promise<{message: string}>} Build status message + * @throws {Error} When index building fails + */ async buildIndex(indexId: string, opts: { latechunk?: boolean; doclingChunk?: boolean; @@ -509,6 +722,13 @@ class ChatAPI { } } + /** + * Links an existing index to a chat session for document retrieval. + * @param {string} sessionId - ID of the session to link the index to + * @param {string} indexId - ID of the index to link + * @returns {Promise<{message: string}>} Link status message + * @throws {Error} When linking fails + */ async linkIndexToSession(sessionId: string, indexId: string): Promise<{ message: string }> { const resp = await fetch(`${API_BASE_URL}/sessions/${sessionId}/indexes/${indexId}`, { method: 'POST' }); if (!resp.ok) { @@ -518,6 +738,11 @@ class ChatAPI { return resp.json(); } + /** + * Retrieves all available document indexes. + * @returns {Promise<{indexes: any[], total: number}>} List of indexes with total count + * @throws {Error} When the indexes cannot be retrieved + */ async listIndexes(): Promise<{ indexes: any[]; total: number }> { const resp = await fetch(`${API_BASE_URL}/indexes`); if (!resp.ok) { @@ -526,12 +751,24 @@ class ChatAPI { return resp.json(); } + /** + * Retrieves indexes linked to a specific session. + * @param {string} sessionId - ID of the session to get indexes for + * @returns {Promise<{indexes: any[], total: number}>} Session's linked indexes + * @throws {Error} When the session indexes cannot be retrieved + */ async getSessionIndexes(sessionId: string): Promise<{ indexes: any[]; total: number }> { const resp = await fetch(`${API_BASE_URL}/sessions/${sessionId}/indexes`); if (!resp.ok) throw new Error(`Failed to get session indexes: ${resp.status}`); return resp.json(); } + /** + * Permanently deletes a document index and all its data. + * @param {string} indexId - ID of the index to delete + * @returns {Promise<{message: string}>} Deletion confirmation message + * @throws {Error} When the index deletion fails + */ async deleteIndex(indexId: string): Promise<{ message: string }> { const resp = await fetch(`${API_BASE_URL}/indexes/${indexId}`, { method: 'DELETE', @@ -543,7 +780,29 @@ class ChatAPI { return resp.json(); } - // -------------------- Streaming (SSE-over-fetch) -------------------- + /** + * Streams a chat session message using Server-Sent Events over fetch. + * @param {Object} params - Streaming parameters + * @param {string} params.query - The query/message to send + * @param {string} [params.model] - AI model to use + * @param {string} [params.session_id] - Session ID for context + * @param {string} [params.table_name] - Table name for data retrieval + * @param {boolean} [params.composeSubAnswers] - Whether to compose sub-answers + * @param {boolean} [params.decompose] - Whether to decompose the query + * @param {boolean} [params.aiRerank] - Whether to use AI reranking + * @param {boolean} [params.contextExpand] - Whether to expand context + * @param {boolean} [params.verify] - Whether to verify responses + * @param {number} [params.retrievalK] - Number of documents to retrieve + * @param {number} [params.contextWindowSize] - Context window size + * @param {number} [params.rerankerTopK] - Top K for reranking + * @param {string} [params.searchType] - Type of search to perform + * @param {number} [params.denseWeight] - Weight for dense retrieval + * @param {boolean} [params.forceRag] - Whether to force RAG usage + * @param {boolean} [params.provencePrune] - Whether to prune provenance + * @param {function} onEvent - Callback function to handle streaming events + * @returns {Promise} Resolves when streaming completes + * @throws {Error} When the streaming request fails + */ async streamSessionMessage( params: { query: string; @@ -606,13 +865,13 @@ class ChatAPI { if (done) break; buffer += decoder.decode(value, { stream: true }); - const parts = buffer.split('\n\n'); + const parts = buffer.split('\\n\\n'); buffer = parts.pop() || ''; for (const part of parts) { const line = part.trim(); if (!line.startsWith('data:')) continue; - const jsonStr = line.replace(/^data:\s*/, ''); + const jsonStr = line.replace(/^data:\\s*/, ''); try { const evt = JSON.parse(jsonStr); onEvent(evt); @@ -630,4 +889,5 @@ class ChatAPI { } } -export const chatAPI = new ChatAPI(); \ No newline at end of file +/** Singleton instance of the ChatAPI for use throughout the application */ +export const chatAPI = new ChatAPI(); \ No newline at end of file