import json import platform from starlette.responses import JSONResponse from utils.logging_config import get_logger from config.settings import ( LANGFLOW_URL, LANGFLOW_CHAT_FLOW_ID, LANGFLOW_INGEST_FLOW_ID, LANGFLOW_PUBLIC_URL, clients, get_openrag_config, config_manager, ) logger = get_logger(__name__) # Docling preset configurations def get_docling_preset_configs(): """Get docling preset configurations with platform-specific settings""" is_macos = platform.system() == "Darwin" return { "standard": { "do_ocr": False }, "ocr": { "do_ocr": True, "ocr_engine": "ocrmac" if is_macos else "easyocr" }, "picture_description": { "do_ocr": True, "ocr_engine": "ocrmac" if is_macos else "easyocr", "do_picture_classification": True, "do_picture_description": True, "picture_description_local": { "repo_id": "HuggingFaceTB/SmolVLM-256M-Instruct", "prompt": "Describe this image in a few sentences." } }, "VLM": { "pipeline": "vlm", "vlm_pipeline_model_local": { "repo_id": "ds4sd/SmolDocling-256M-preview-mlx-bf16" if is_macos else "ds4sd/SmolDocling-256M-preview", "response_format": "doctags", "inference_framework": "mlx" } } } def get_docling_tweaks(docling_preset: str = None) -> dict: """Get Langflow tweaks for docling component based on preset""" if not docling_preset: # Get current preset from config openrag_config = get_openrag_config() docling_preset = openrag_config.knowledge.doclingPresets preset_configs = get_docling_preset_configs() if docling_preset not in preset_configs: docling_preset = "standard" # fallback preset_config = preset_configs[docling_preset] docling_serve_opts = json.dumps(preset_config) return { "DoclingRemote-ayRdw": { "docling_serve_opts": docling_serve_opts } } async def get_settings(request, session_manager): """Get application settings""" try: openrag_config = get_openrag_config() provider_config = openrag_config.provider knowledge_config = openrag_config.knowledge agent_config = openrag_config.agent # Return public settings that are safe to expose to frontend settings = { "langflow_url": LANGFLOW_URL, "flow_id": LANGFLOW_CHAT_FLOW_ID, "ingest_flow_id": LANGFLOW_INGEST_FLOW_ID, "langflow_public_url": LANGFLOW_PUBLIC_URL, "edited": openrag_config.edited, # OpenRAG configuration "provider": { "model_provider": provider_config.model_provider, # Note: API key is not exposed for security }, "knowledge": { "embedding_model": knowledge_config.embedding_model, "chunk_size": knowledge_config.chunk_size, "chunk_overlap": knowledge_config.chunk_overlap, "doclingPresets": knowledge_config.doclingPresets, }, "agent": { "llm_model": agent_config.llm_model, "system_prompt": agent_config.system_prompt, }, } # Only expose edit URLs when a public URL is configured if LANGFLOW_PUBLIC_URL and LANGFLOW_CHAT_FLOW_ID: settings["langflow_edit_url"] = ( f"{LANGFLOW_PUBLIC_URL.rstrip('/')}/flow/{LANGFLOW_CHAT_FLOW_ID}" ) if LANGFLOW_PUBLIC_URL and LANGFLOW_INGEST_FLOW_ID: settings["langflow_ingest_edit_url"] = ( f"{LANGFLOW_PUBLIC_URL.rstrip('/')}/flow/{LANGFLOW_INGEST_FLOW_ID}" ) # Fetch ingestion flow configuration to get actual component defaults if LANGFLOW_INGEST_FLOW_ID and openrag_config.edited: try: response = await clients.langflow_request( "GET", f"/api/v1/flows/{LANGFLOW_INGEST_FLOW_ID}" ) if response.status_code == 200: flow_data = response.json() # Extract component defaults (ingestion-specific settings only) # Start with configured defaults ingestion_defaults = { "chunkSize": knowledge_config.chunk_size, "chunkOverlap": knowledge_config.chunk_overlap, "separator": "\\n", # Keep hardcoded for now as it's not in config "embeddingModel": knowledge_config.embedding_model, } if flow_data.get("data", {}).get("nodes"): for node in flow_data["data"]["nodes"]: node_template = ( node.get("data", {}).get("node", {}).get("template", {}) ) # Split Text component (SplitText-QIKhg) if node.get("id") == "SplitText-QIKhg": if node_template.get("chunk_size", {}).get("value"): ingestion_defaults["chunkSize"] = node_template[ "chunk_size" ]["value"] if node_template.get("chunk_overlap", {}).get("value"): ingestion_defaults["chunkOverlap"] = node_template[ "chunk_overlap" ]["value"] if node_template.get("separator", {}).get("value"): ingestion_defaults["separator"] = node_template[ "separator" ]["value"] # OpenAI Embeddings component (OpenAIEmbeddings-joRJ6) elif node.get("id") == "OpenAIEmbeddings-joRJ6": if node_template.get("model", {}).get("value"): ingestion_defaults["embeddingModel"] = ( node_template["model"]["value"] ) # Note: OpenSearch component settings are not exposed for ingestion # (search-related parameters like number_of_results, score_threshold # are for retrieval, not ingestion) settings["ingestion_defaults"] = ingestion_defaults except Exception as e: logger.warning(f"Failed to fetch ingestion flow defaults: {e}") # Continue without ingestion defaults return JSONResponse(settings) except Exception as e: return JSONResponse( {"error": f"Failed to retrieve settings: {str(e)}"}, status_code=500 ) async def update_settings(request, session_manager): """Update application settings""" try: # Get current configuration current_config = get_openrag_config() # Check if config is marked as edited if not current_config.edited: return JSONResponse( { "error": "Configuration must be marked as edited before updates are allowed" }, status_code=403, ) # Parse request body body = await request.json() # Validate allowed fields allowed_fields = { "llm_model", "system_prompt", "chunk_size", "chunk_overlap", "doclingPresets", } # Check for invalid fields invalid_fields = set(body.keys()) - allowed_fields if invalid_fields: return JSONResponse( { "error": f"Invalid fields: {', '.join(invalid_fields)}. Allowed fields: {', '.join(allowed_fields)}" }, status_code=400, ) # Update configuration config_updated = False # Update agent settings if "llm_model" in body: current_config.agent.llm_model = body["llm_model"] config_updated = True if "system_prompt" in body: current_config.agent.system_prompt = body["system_prompt"] config_updated = True # Update knowledge settings if "doclingPresets" in body: preset_configs = get_docling_preset_configs() valid_presets = list(preset_configs.keys()) if body["doclingPresets"] not in valid_presets: return JSONResponse( { "error": f"doclingPresets must be one of: {', '.join(valid_presets)}" }, status_code=400, ) current_config.knowledge.doclingPresets = body["doclingPresets"] config_updated = True if "chunk_size" in body: if not isinstance(body["chunk_size"], int) or body["chunk_size"] <= 0: return JSONResponse( {"error": "chunk_size must be a positive integer"}, status_code=400 ) current_config.knowledge.chunk_size = body["chunk_size"] config_updated = True if "chunk_overlap" in body: if not isinstance(body["chunk_overlap"], int) or body["chunk_overlap"] < 0: return JSONResponse( {"error": "chunk_overlap must be a non-negative integer"}, status_code=400, ) current_config.knowledge.chunk_overlap = body["chunk_overlap"] config_updated = True if not config_updated: return JSONResponse( {"error": "No valid fields provided for update"}, status_code=400 ) # Save the updated configuration if config_manager.save_config_file(current_config): logger.info( "Configuration updated successfully", updated_fields=list(body.keys()) ) return JSONResponse({"message": "Configuration updated successfully"}) else: return JSONResponse( {"error": "Failed to save configuration"}, status_code=500 ) except Exception as e: logger.error("Failed to update settings", error=str(e)) return JSONResponse( {"error": f"Failed to update settings: {str(e)}"}, status_code=500 ) async def onboarding(request, flows_service): """Handle onboarding configuration setup""" try: # Get current configuration current_config = get_openrag_config() # Check if config is NOT marked as edited (only allow onboarding if not yet configured) if current_config.edited: return JSONResponse( { "error": "Configuration has already been edited. Use /settings endpoint for updates." }, status_code=403, ) # Parse request body body = await request.json() # Validate allowed fields allowed_fields = { "model_provider", "api_key", "embedding_model", "llm_model", "sample_data", "endpoint", "project_id", } # Check for invalid fields invalid_fields = set(body.keys()) - allowed_fields if invalid_fields: return JSONResponse( { "error": f"Invalid fields: {', '.join(invalid_fields)}. Allowed fields: {', '.join(allowed_fields)}" }, status_code=400, ) # Update configuration config_updated = False # Update provider settings if "model_provider" in body: if ( not isinstance(body["model_provider"], str) or not body["model_provider"].strip() ): return JSONResponse( {"error": "model_provider must be a non-empty string"}, status_code=400, ) current_config.provider.model_provider = body["model_provider"].strip() config_updated = True if "api_key" in body: if not isinstance(body["api_key"], str): return JSONResponse( {"error": "api_key must be a string"}, status_code=400 ) current_config.provider.api_key = body["api_key"] config_updated = True # Update knowledge settings if "embedding_model" in body: if ( not isinstance(body["embedding_model"], str) or not body["embedding_model"].strip() ): return JSONResponse( {"error": "embedding_model must be a non-empty string"}, status_code=400, ) current_config.knowledge.embedding_model = body["embedding_model"].strip() config_updated = True # Update agent settings if "llm_model" in body: if not isinstance(body["llm_model"], str) or not body["llm_model"].strip(): return JSONResponse( {"error": "llm_model must be a non-empty string"}, status_code=400 ) current_config.agent.llm_model = body["llm_model"].strip() config_updated = True if "endpoint" in body: if not isinstance(body["endpoint"], str) or not body["endpoint"].strip(): return JSONResponse( {"error": "endpoint must be a non-empty string"}, status_code=400 ) current_config.provider.endpoint = body["endpoint"].strip() config_updated = True if "project_id" in body: if ( not isinstance(body["project_id"], str) or not body["project_id"].strip() ): return JSONResponse( {"error": "project_id must be a non-empty string"}, status_code=400 ) current_config.provider.project_id = body["project_id"].strip() config_updated = True # Handle sample_data should_ingest_sample_data = False if "sample_data" in body: if not isinstance(body["sample_data"], bool): return JSONResponse( {"error": "sample_data must be a boolean value"}, status_code=400 ) should_ingest_sample_data = body["sample_data"] if not config_updated: return JSONResponse( {"error": "No valid fields provided for update"}, status_code=400 ) # Save the updated configuration (this will mark it as edited) if config_manager.save_config_file(current_config): updated_fields = [ k for k in body.keys() if k != "sample_data" ] # Exclude sample_data from log logger.info( "Onboarding configuration updated successfully", updated_fields=updated_fields, ) # If model_provider was updated, assign the new provider to flows if "model_provider" in body: provider = body["model_provider"].strip().lower() try: flow_result = await flows_service.assign_model_provider(provider) if flow_result.get("success"): logger.info( f"Successfully assigned {provider} to flows", flow_result=flow_result, ) else: logger.warning( f"Failed to assign {provider} to flows", flow_result=flow_result, ) # Continue even if flow assignment fails - configuration was still saved except Exception as e: logger.error( "Error assigning model provider to flows", provider=provider, error=str(e), ) # Continue even if flow assignment fails - configuration was still saved # Handle sample data ingestion if requested if should_ingest_sample_data: try: # Import the function here to avoid circular imports from main import ingest_default_documents_when_ready # Get services from the current app state # We need to access the app instance to get services app = request.scope.get("app") if app and hasattr(app.state, "services"): services = app.state.services logger.info( "Starting sample data ingestion as requested in onboarding" ) await ingest_default_documents_when_ready(services) logger.info("Sample data ingestion completed successfully") else: logger.error( "Could not access services for sample data ingestion" ) except Exception as e: logger.error( "Failed to complete sample data ingestion", error=str(e) ) # Don't fail the entire onboarding process if sample data fails return JSONResponse( { "message": "Onboarding configuration updated successfully", "edited": True, # Confirm that config is now marked as edited "sample_data_ingested": should_ingest_sample_data, } ) else: return JSONResponse( {"error": "Failed to save configuration"}, status_code=500 ) except Exception as e: logger.error("Failed to update onboarding settings", error=str(e)) return JSONResponse( {"error": f"Failed to update onboarding settings: {str(e)}"}, status_code=500, )