Added settings service and added post endpoint to edit them

This commit is contained in:
Lucas Oliveira 2025-09-17 16:50:19 -03:00
parent 8dc737c124
commit 03b827f3e0
6 changed files with 475 additions and 28 deletions

33
config.yaml Normal file
View file

@ -0,0 +1,33 @@
# OpenRAG Configuration File
# This file allows you to configure OpenRAG settings.
# Environment variables will override these settings unless edited is true.
# Track if this config has been manually edited (prevents env var overrides)
edited: false
# Model provider configuration
provider:
# Supported providers: "openai", "anthropic", "azure", etc.
model_provider: "openai"
# API key for the model provider (can also be set via OPENAI_API_KEY env var)
api_key: ""
# Knowledge base and document processing configuration
knowledge:
# Embedding model for vector search
embedding_model: "text-embedding-3-small"
# Text chunk size for document processing
chunk_size: 1000
# Overlap between chunks
chunk_overlap: 200
# Enable OCR for image processing
ocr: true
# Enable picture descriptions using vision models
picture_descriptions: false
# AI agent configuration
agent:
# Language model for the chat agent
llm_model: "gpt-4o-mini"
# System prompt for the agent
system_prompt: "You are a helpful AI assistant with access to a knowledge base. Answer questions based on the provided context."

View file

@ -1,6 +1,37 @@
# Configuration
OpenRAG uses environment variables for configuration. Copy `.env.example` to `.env` and populate with your values:
OpenRAG supports multiple configuration methods with the following priority:
1. **Environment Variables** (highest priority)
2. **Configuration File** (`config.yaml`)
3. **Langflow Flow Settings** (runtime override)
4. **Default Values** (fallback)
## Configuration File
Create a `config.yaml` file in the project root to configure OpenRAG:
```yaml
# OpenRAG Configuration File
provider:
model_provider: "openai" # openai, anthropic, azure, etc.
api_key: "your-api-key" # or use OPENAI_API_KEY env var
knowledge:
embedding_model: "text-embedding-3-small"
chunk_size: 1000
chunk_overlap: 200
ocr: true
picture_descriptions: false
agent:
llm_model: "gpt-4o-mini"
system_prompt: "You are a helpful AI assistant..."
```
## Environment Variables
Environment variables will override configuration file settings. You can still use `.env` files:
```bash
cp .env.example .env
@ -8,20 +39,20 @@ cp .env.example .env
## Required Variables
| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | Your OpenAI API key |
| `OPENSEARCH_PASSWORD` | Password for OpenSearch admin user |
| `LANGFLOW_SUPERUSER` | Langflow admin username |
| `LANGFLOW_SUPERUSER_PASSWORD` | Langflow admin password |
| `LANGFLOW_CHAT_FLOW_ID` | ID of your Langflow chat flow |
| `LANGFLOW_INGEST_FLOW_ID` | ID of your Langflow ingestion flow |
| `NUDGES_FLOW_ID` | ID of your Langflow nudges/suggestions flow |
| Variable | Description |
| ----------------------------- | ------------------------------------------- |
| `OPENAI_API_KEY` | Your OpenAI API key |
| `OPENSEARCH_PASSWORD` | Password for OpenSearch admin user |
| `LANGFLOW_SUPERUSER` | Langflow admin username |
| `LANGFLOW_SUPERUSER_PASSWORD` | Langflow admin password |
| `LANGFLOW_CHAT_FLOW_ID` | ID of your Langflow chat flow |
| `LANGFLOW_INGEST_FLOW_ID` | ID of your Langflow ingestion flow |
| `NUDGES_FLOW_ID` | ID of your Langflow nudges/suggestions flow |
## Ingestion Configuration
| Variable | Description |
|----------|-------------|
| Variable | Description |
| ------------------------------ | ------------------------------------------------------ |
| `DISABLE_INGEST_WITH_LANGFLOW` | Disable Langflow ingestion pipeline (default: `false`) |
- `false` or unset: Uses Langflow pipeline (upload → ingest → delete)
@ -29,15 +60,44 @@ cp .env.example .env
## Optional Variables
| Variable | Description |
|----------|-------------|
| `LANGFLOW_PUBLIC_URL` | Public URL for Langflow (default: `http://localhost:7860`) |
| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth authentication |
| `MICROSOFT_GRAPH_OAUTH_CLIENT_ID` / `MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET` | Microsoft OAuth |
| `WEBHOOK_BASE_URL` | Base URL for webhook endpoints |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | AWS integrations |
| `SESSION_SECRET` | Session management (default: auto-generated, change in production) |
| `LANGFLOW_KEY` | Explicit Langflow API key (auto-generated if not provided) |
| `LANGFLOW_SECRET_KEY` | Secret key for Langflow internal operations |
| Variable | Description |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `LANGFLOW_PUBLIC_URL` | Public URL for Langflow (default: `http://localhost:7860`) |
| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth authentication |
| `MICROSOFT_GRAPH_OAUTH_CLIENT_ID` / `MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET` | Microsoft OAuth |
| `WEBHOOK_BASE_URL` | Base URL for webhook endpoints |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | AWS integrations |
| `SESSION_SECRET` | Session management (default: auto-generated, change in production) |
| `LANGFLOW_KEY` | Explicit Langflow API key (auto-generated if not provided) |
| `LANGFLOW_SECRET_KEY` | Secret key for Langflow internal operations |
## OpenRAG Configuration Variables
These environment variables override settings in `config.yaml`:
### Provider Settings
| Variable | Description | Default |
| ------------------ | ---------------------------------------- | -------- |
| `MODEL_PROVIDER` | Model provider (openai, anthropic, etc.) | `openai` |
| `PROVIDER_API_KEY` | API key for the model provider | |
| `OPENAI_API_KEY` | OpenAI API key (backward compatibility) | |
### Knowledge Settings
| Variable | Description | Default |
| ------------------------------ | --------------------------------------- | ------------------------ |
| `EMBEDDING_MODEL` | Embedding model for vector search | `text-embedding-3-small` |
| `CHUNK_SIZE` | Text chunk size for document processing | `1000` |
| `CHUNK_OVERLAP` | Overlap between chunks | `200` |
| `OCR_ENABLED` | Enable OCR for image processing | `true` |
| `PICTURE_DESCRIPTIONS_ENABLED` | Enable picture descriptions | `false` |
### Agent Settings
| Variable | Description | Default |
| --------------- | --------------------------------- | ------------------------ |
| `LLM_MODEL` | Language model for the chat agent | `gpt-4o-mini` |
| `SYSTEM_PROMPT` | System prompt for the agent | Default assistant prompt |
See `.env.example` for a complete list with descriptions, and `docker-compose*.yml` for runtime usage.

View file

@ -6,6 +6,8 @@ from config.settings import (
LANGFLOW_INGEST_FLOW_ID,
LANGFLOW_PUBLIC_URL,
clients,
get_openrag_config,
config_manager,
)
logger = get_logger(__name__)
@ -15,12 +17,34 @@ logger = get_logger(__name__)
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,
"ocr": knowledge_config.ocr,
"picture_descriptions": knowledge_config.picture_descriptions,
},
"agent": {
"llm_model": agent_config.llm_model,
"system_prompt": agent_config.system_prompt,
},
}
# Only expose edit URLs when a public URL is configured
@ -35,7 +59,7 @@ async def get_settings(request, session_manager):
)
# Fetch ingestion flow configuration to get actual component defaults
if LANGFLOW_INGEST_FLOW_ID:
if LANGFLOW_INGEST_FLOW_ID and openrag_config.edited:
try:
response = await clients.langflow_request(
"GET",
@ -45,11 +69,12 @@ async def get_settings(request, session_manager):
flow_data = response.json()
# Extract component defaults (ingestion-specific settings only)
# Start with configured defaults
ingestion_defaults = {
"chunkSize": 1000,
"chunkOverlap": 200,
"separator": "\\n",
"embeddingModel": "text-embedding-3-small",
"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"):
@ -104,3 +129,105 @@ async def get_settings(request, session_manager):
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", "ocr", "picture_descriptions",
"chunk_size", "chunk_overlap"
}
# 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 "ocr" in body:
if not isinstance(body["ocr"], bool):
return JSONResponse(
{"error": "ocr must be a boolean value"},
status_code=400
)
current_config.knowledge.ocr = body["ocr"]
config_updated = True
if "picture_descriptions" in body:
if not isinstance(body["picture_descriptions"], bool):
return JSONResponse(
{"error": "picture_descriptions must be a boolean value"},
status_code=400
)
current_config.knowledge.picture_descriptions = body["picture_descriptions"]
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
)

View file

@ -0,0 +1,197 @@
"""Configuration management for OpenRAG."""
import os
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
from dataclasses import dataclass, asdict
from utils.logging_config import get_logger
logger = get_logger(__name__)
@dataclass
class ProviderConfig:
"""Model provider configuration."""
model_provider: str = "openai" # openai, anthropic, etc.
api_key: str = ""
@dataclass
class KnowledgeConfig:
"""Knowledge/ingestion configuration."""
embedding_model: str = "text-embedding-3-small"
chunk_size: int = 1000
chunk_overlap: int = 200
ocr: bool = True
picture_descriptions: bool = False
@dataclass
class AgentConfig:
"""Agent configuration."""
llm_model: str = "gpt-4o-mini"
system_prompt: str = "You are a helpful AI assistant with access to a knowledge base. Answer questions based on the provided context."
@dataclass
class OpenRAGConfig:
"""Complete OpenRAG configuration."""
provider: ProviderConfig
knowledge: KnowledgeConfig
agent: AgentConfig
edited: bool = False # Track if manually edited
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "OpenRAGConfig":
"""Create config from dictionary."""
return cls(
provider=ProviderConfig(**data.get("provider", {})),
knowledge=KnowledgeConfig(**data.get("knowledge", {})),
agent=AgentConfig(**data.get("agent", {}))
)
def to_dict(self) -> Dict[str, Any]:
"""Convert config to dictionary."""
return asdict(self)
class ConfigManager:
"""Manages OpenRAG configuration from multiple sources."""
def __init__(self, config_file: Optional[str] = None):
"""Initialize configuration manager.
Args:
config_file: Path to configuration file. Defaults to 'config.yaml' in project root.
"""
self.config_file = Path(config_file) if config_file else Path("config.yaml")
self._config: Optional[OpenRAGConfig] = None
def load_config(self) -> OpenRAGConfig:
"""Load configuration from environment variables and config file.
Priority order:
1. Environment variables (highest)
2. Configuration file
3. Defaults (lowest)
"""
if self._config is not None:
return self._config
# Start with defaults
config_data = {
"provider": {},
"knowledge": {},
"agent": {}
}
# Load from config file if it exists
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
file_config = yaml.safe_load(f) or {}
# Merge file config
for section in ["provider", "knowledge", "agent"]:
if section in file_config:
config_data[section].update(file_config[section])
logger.info(f"Loaded configuration from {self.config_file}")
except Exception as e:
logger.warning(f"Failed to load config file {self.config_file}: {e}")
# Create config object first to check edited flags
temp_config = OpenRAGConfig.from_dict(config_data)
# Override with environment variables (highest priority, but respect edited flags)
self._load_env_overrides(config_data, temp_config)
# Create config object
self._config = OpenRAGConfig.from_dict(config_data)
logger.debug("Configuration loaded", config=self._config.to_dict())
return self._config
def _load_env_overrides(self, config_data: Dict[str, Any], temp_config: Optional["OpenRAGConfig"] = None) -> None:
"""Load environment variable overrides, respecting edited flag."""
# Skip all environment overrides if config has been manually edited
if temp_config and temp_config.edited:
logger.debug("Skipping all env overrides - config marked as edited")
return
# Provider settings
if os.getenv("MODEL_PROVIDER"):
config_data["provider"]["model_provider"] = os.getenv("MODEL_PROVIDER")
if os.getenv("PROVIDER_API_KEY"):
config_data["provider"]["api_key"] = os.getenv("PROVIDER_API_KEY")
# Backward compatibility for OpenAI
if os.getenv("OPENAI_API_KEY"):
config_data["provider"]["api_key"] = os.getenv("OPENAI_API_KEY")
if not config_data["provider"].get("model_provider"):
config_data["provider"]["model_provider"] = "openai"
# Knowledge settings
if os.getenv("EMBEDDING_MODEL"):
config_data["knowledge"]["embedding_model"] = os.getenv("EMBEDDING_MODEL")
if os.getenv("CHUNK_SIZE"):
config_data["knowledge"]["chunk_size"] = int(os.getenv("CHUNK_SIZE"))
if os.getenv("CHUNK_OVERLAP"):
config_data["knowledge"]["chunk_overlap"] = int(os.getenv("CHUNK_OVERLAP"))
if os.getenv("OCR_ENABLED"):
config_data["knowledge"]["ocr"] = os.getenv("OCR_ENABLED").lower() in ("true", "1", "yes")
if os.getenv("PICTURE_DESCRIPTIONS_ENABLED"):
config_data["knowledge"]["picture_descriptions"] = os.getenv("PICTURE_DESCRIPTIONS_ENABLED").lower() in ("true", "1", "yes")
# Agent settings
if os.getenv("LLM_MODEL"):
config_data["agent"]["llm_model"] = os.getenv("LLM_MODEL")
if os.getenv("SYSTEM_PROMPT"):
config_data["agent"]["system_prompt"] = os.getenv("SYSTEM_PROMPT")
def get_config(self) -> OpenRAGConfig:
"""Get current configuration, loading if necessary."""
if self._config is None:
return self.load_config()
return self._config
def reload_config(self) -> OpenRAGConfig:
"""Force reload configuration from sources."""
self._config = None
return self.load_config()
def save_config_file(self, config: Optional[OpenRAGConfig] = None) -> bool:
"""Save configuration to file.
Args:
config: Configuration to save. If None, uses current config.
Returns:
True if saved successfully, False otherwise.
"""
if config is None:
config = self.get_config()
# Mark config as edited when saving
config.edited = True
try:
# Ensure directory exists
self.config_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w') as f:
yaml.dump(config.to_dict(), f, default_flow_style=False, indent=2)
# Update cached config to reflect the edited flags
self._config = config
logger.info(f"Configuration saved to {self.config_file} - marked as edited")
return True
except Exception as e:
logger.error(f"Failed to save configuration to {self.config_file}: {e}")
return False
# Global config manager instance
config_manager = ConfigManager()

View file

@ -17,6 +17,9 @@ load_dotenv("../")
logger = get_logger(__name__)
# Import configuration manager
from .config_manager import config_manager
# Environment variables
OPENSEARCH_HOST = os.getenv("OPENSEARCH_HOST", "localhost")
OPENSEARCH_PORT = int(os.getenv("OPENSEARCH_PORT", "9200"))
@ -398,3 +401,21 @@ class AppClients:
# Global clients instance
clients = AppClients()
# Configuration access
def get_openrag_config():
"""Get current OpenRAG configuration."""
return config_manager.get_config()
# Expose configuration settings for backward compatibility and easy access
def get_provider_config():
"""Get provider configuration."""
return get_openrag_config().provider
def get_knowledge_config():
"""Get knowledge configuration."""
return get_openrag_config().knowledge
def get_agent_config():
"""Get agent configuration."""
return get_openrag_config().agent

View file

@ -890,7 +890,7 @@ async def create_app():
),
methods=["POST"],
),
# Settings endpoint
# Settings endpoints
Route(
"/settings",
require_auth(services["session_manager"])(
@ -900,6 +900,15 @@ async def create_app():
),
methods=["GET"],
),
Route(
"/settings",
require_auth(services["session_manager"])(
partial(
settings.update_settings, session_manager=services["session_manager"]
)
),
methods=["POST"],
),
Route(
"/nudges",
require_auth(services["session_manager"])(