320 lines
12 KiB
Python
Executable File
320 lines
12 KiB
Python
Executable File
"""
|
|
AI-LSC — LibreChat configuration generator.
|
|
|
|
Generates the ``librechat.yaml`` configuration file that wires LibreChat
|
|
to the local AI-LSC stack: Ollama inference engine, LiteLLM proxy for
|
|
multi-model routing, and the agent tool-use schemas from the agents bridge.
|
|
|
|
This makes LibreChat the turnkey agent frontend for the agentic stack
|
|
— just start it and all 210+ models plus tool-calling are available
|
|
through the web UI.
|
|
|
|
Usage
|
|
-----
|
|
config = LibreChatConfigGenerator()
|
|
config.set_ollama_endpoint(ollama_port=11434)
|
|
config.set_litellm_endpoint(litellm_port=4000)
|
|
config.set_tool_schemas(tool_schemas)
|
|
config.save("/mnt/AI/dashboards/librechat")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ai_lsc.utils.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def _env_api_key(env_var: str, default: str = "") -> str:
|
|
"""Read an API key from the process environment.
|
|
|
|
Returns *default* (empty string by default) when the variable is unset.
|
|
Never logs the value.
|
|
"""
|
|
return os.environ.get(env_var, default)
|
|
|
|
|
|
class LibreChatConfigGenerator:
|
|
"""Generate LibreChat configuration for AI-LSC integration.
|
|
|
|
Parameters
|
|
----------
|
|
config_dir :
|
|
Directory where LibreChat is installed (contains docker-compose.yml
|
|
or the yarn project root).
|
|
"""
|
|
|
|
def __init__(self, config_dir: str | Path | None = None) -> None:
|
|
self.config_dir = Path(config_dir) if config_dir else None
|
|
self._endpoints: dict[str, dict[str, Any]] = {}
|
|
self._tool_schemas: list[dict[str, Any]] = []
|
|
self._assistants: list[dict[str, Any]] = []
|
|
self._preset_customizations: list[dict[str, Any]] = []
|
|
|
|
# ── Endpoint Configuration ─────────────────────────────────────────
|
|
|
|
def set_ollama_endpoint(
|
|
self,
|
|
ollama_port: int = 11434,
|
|
ollama_host: str = "127.0.0.1",
|
|
) -> None:
|
|
"""Configure the direct Ollama endpoint for native tool calling."""
|
|
self._endpoints["ollama"] = {
|
|
"type": "ollama",
|
|
"name": "AI-LSC Ollama (Native Tool Calling)",
|
|
"url": f"http://{ollama_host}:{ollama_port}",
|
|
"models": {
|
|
"default": ["qwen2.5:32b", "qwen2.5:72b"],
|
|
"fetch": True, # auto-discover models from /api/tags
|
|
},
|
|
}
|
|
|
|
def set_litellm_endpoint(
|
|
self,
|
|
litellm_port: int = 4000,
|
|
litellm_host: str = "127.0.0.1",
|
|
api_key: str | None = None,
|
|
) -> None:
|
|
"""Configure the LiteLLM proxy endpoint for multi-model routing.
|
|
|
|
``api_key`` defaults to the ``AI_LSC_LITELLM_KEY`` environment
|
|
variable. If neither is set, an empty string is written and the
|
|
caller is expected to supply the key out-of-band.
|
|
"""
|
|
resolved_key = api_key or _env_api_key("AI_LSC_LITELLM_KEY")
|
|
self._endpoints["litellm"] = {
|
|
"type": "openai",
|
|
"name": "AI-LSC LiteLLM Proxy (All Models)",
|
|
"url": f"http://{litellm_host}:{litellm_port}/v1",
|
|
"apiKey": resolved_key,
|
|
"models": {
|
|
"default": [
|
|
"classifier", "utility", "reasoner", "heavy",
|
|
"llama3-8b", "gemma2-9b", "phi4", "mistral",
|
|
"command-r", "coder-heavy",
|
|
],
|
|
"fetch": True,
|
|
},
|
|
}
|
|
|
|
def set_openwebui_endpoint(
|
|
self,
|
|
port: int = 8080,
|
|
host: str = "127.0.0.1",
|
|
api_key: str | None = None,
|
|
) -> None:
|
|
"""Configure OpenWebUI as an OpenAI-compatible endpoint."""
|
|
resolved_key = api_key or _env_api_key("AI_LSC_OPENWEBUI_KEY")
|
|
self._endpoints["openwebui"] = {
|
|
"type": "openai",
|
|
"name": "Open WebUI",
|
|
"url": f"http://{host}:{port}/api",
|
|
"apiKey": resolved_key,
|
|
"models": {"default": ["*"], "fetch": True},
|
|
}
|
|
|
|
# ── Tool Schema Integration ────────────────────────────────────────
|
|
|
|
def set_tool_schemas(
|
|
self,
|
|
schemas: list[dict[str, Any]],
|
|
) -> None:
|
|
"""Set the AI-LSC tool schemas for LibreChat's tool-use system.
|
|
|
|
These are registered as server-side tools that any assistant
|
|
can invoke through the OpenAI function-calling protocol.
|
|
"""
|
|
self._tool_schemas = schemas
|
|
|
|
# ── Assistant Presets ───────────────────────────────────────────────
|
|
|
|
def add_assistant_preset(
|
|
self,
|
|
name: str,
|
|
model: str = "reasoner",
|
|
system_prompt: str = "",
|
|
tools_enabled: bool = True,
|
|
) -> None:
|
|
"""Add a pre-configured assistant definition.
|
|
|
|
Parameters
|
|
----------
|
|
name :
|
|
Assistant display name.
|
|
model :
|
|
Default model identifier (matches LiteLLM alias or Ollama model).
|
|
system_prompt :
|
|
Initial system prompt.
|
|
tools_enabled :
|
|
Whether to enable AI-LSC tool calling.
|
|
"""
|
|
assistant: dict[str, Any] = {
|
|
"name": name,
|
|
"model": model,
|
|
"system_prompt": system_prompt,
|
|
}
|
|
if tools_enabled and self._tool_schemas:
|
|
assistant["tools"] = self._tool_schemas
|
|
self._assistants.append(assistant)
|
|
|
|
def add_default_assistants(self) -> None:
|
|
"""Add the standard AI-LSC assistant presets."""
|
|
self.add_assistant_preset(
|
|
name="Stack Operator",
|
|
model="reasoner",
|
|
system_prompt=(
|
|
"You are the AI-LSC Stack Operator. You can start, stop, "
|
|
"and manage the entire AI tool stack. Use tools to control "
|
|
"services, pull models, and configure the pipeline. "
|
|
"Always check service status before starting or stopping."
|
|
),
|
|
)
|
|
self.add_assistant_preset(
|
|
name="RAG Analyst",
|
|
model="reasoner",
|
|
system_prompt=(
|
|
"You are the AI-LSC RAG Analyst. You search knowledge "
|
|
"bases, analyze documents using vector similarity, and "
|
|
"synthesize information from multiple sources. Use the "
|
|
"inject_skill tool to load the rag-analyst skill."
|
|
),
|
|
)
|
|
self.add_assistant_preset(
|
|
name="Code Reviewer",
|
|
model="heavy",
|
|
system_prompt=(
|
|
"You are the AI-LSC Code Reviewer. You review code for "
|
|
"bugs, style issues, security vulnerabilities, and "
|
|
"architectural problems. Use the inject_skill tool to "
|
|
"load the code-reviewer skill for deep analysis."
|
|
),
|
|
)
|
|
|
|
# ── YAML Generation ─────────────────────────────────────────────────
|
|
|
|
def generate_yaml(self) -> str:
|
|
"""Generate the librechat.yaml configuration content."""
|
|
lines = [
|
|
"# AI-LSC — LibreChat Configuration",
|
|
"# Auto-generated by agents/librechat_config.py",
|
|
"# Connects LibreChat to the local AI-LSC tool stack",
|
|
"",
|
|
]
|
|
|
|
# Endpoints
|
|
if self._endpoints:
|
|
lines.append("endpoints:")
|
|
for name, config in self._endpoints.items():
|
|
lines.append(f' - name: "{config.get("name", name)}"')
|
|
lines.append(f' type: "{config.get("type", "openai")}"')
|
|
lines.append(f' url: "{config.get("url", "")}"')
|
|
if "apiKey" in config:
|
|
lines.append(f' apiKey: "{config["apiKey"]}"')
|
|
lines.append("")
|
|
|
|
# Tool schemas (written as JSON in a comment block for copy-paste)
|
|
if self._tool_schemas:
|
|
lines.append("# AI-LSC Tool Schemas (register via LibreChat admin UI):")
|
|
lines.append("# tools:")
|
|
lines.append(f"# schemas: {json.dumps(self._tool_schemas, indent=4)}")
|
|
lines.append("")
|
|
|
|
# Assistant presets
|
|
if self._assistants:
|
|
lines.append("# AI-LSC Assistant Presets:")
|
|
for assistant in self._assistants:
|
|
lines.append(f"# - name: \"{assistant['name']}\"")
|
|
lines.append(f"# model: \"{assistant['model']}\"")
|
|
lines.append(f"# system_prompt: \"{assistant.get('system_prompt', '')}\"")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def generate_env_file(self) -> str:
|
|
"""Generate the .env file for LibreChat configuration."""
|
|
env_lines = [
|
|
"# AI-LSC — LibreChat Environment",
|
|
"# Auto-generated by agents/librechat_config.py",
|
|
"",
|
|
"# Database (use MariaDB from AI-LSC stack)",
|
|
"DB_HOST=127.0.0.1",
|
|
"DB_PORT=3306",
|
|
"DB_NAME=librechat",
|
|
"DB_USER=librechat",
|
|
"DB_PASS=librechat",
|
|
"",
|
|
"# Redis (use Redis from AI-LSC stack)",
|
|
"REDIS_HOST=127.0.0.1",
|
|
"REDIS_PORT=6379",
|
|
"",
|
|
# Application settings
|
|
"PORT=3080",
|
|
"HOST=127.0.0.1",
|
|
"NODE_ENV=production",
|
|
"API_PLUGINS=false",
|
|
"",
|
|
# AI-LSC integration
|
|
"ALLOWED_ENDPOINTS=ollama,openai,custom",
|
|
"",
|
|
]
|
|
|
|
# Add endpoint-specific env vars
|
|
if "ollama" in self._endpoints:
|
|
ollama_ep = self._endpoints["ollama"]
|
|
env_lines.append(f"# Ollama endpoint")
|
|
env_lines.append(f"OLLAMA_BASE_URL={ollama_ep.get('url', 'http://127.0.0.1:11434')}")
|
|
env_lines.append("")
|
|
|
|
if "litellm" in self._endpoints:
|
|
litellm = self._endpoints["litellm"]
|
|
env_lines.append(f"# LiteLLM proxy endpoint")
|
|
env_lines.append(f"OPENAI_REVERSE_PROXY={litellm.get('url', 'http://127.0.0.1:4000/v1')}")
|
|
env_lines.append(f"OPENAI_API_KEY={litellm.get('apiKey', _env_api_key('AI_LSC_LITELLM_KEY'))}")
|
|
env_lines.append("")
|
|
|
|
return "\n".join(env_lines)
|
|
|
|
# ── Persistence ──────────────────────────────────────────────────
|
|
|
|
def save(
|
|
self,
|
|
config_dir: str | Path | None = None,
|
|
) -> dict[str, str]:
|
|
"""Write configuration files to disk.
|
|
|
|
Returns a dict mapping filename → absolute path.
|
|
"""
|
|
out_dir = Path(config_dir) if config_dir else self.config_dir
|
|
if not out_dir:
|
|
return {}
|
|
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
written: dict[str, str] = {}
|
|
|
|
# librechat.yaml
|
|
yaml_path = out_dir / "librechat.yaml"
|
|
yaml_path.write_text(self.generate_yaml(), encoding="utf-8")
|
|
written["librechat.yaml"] = str(yaml_path)
|
|
|
|
# .env
|
|
env_path = out_dir / ".env"
|
|
env_path.write_text(self.generate_env_file(), encoding="utf-8")
|
|
written[".env"] = str(env_path)
|
|
|
|
# tool_schemas.json (for import via admin UI)
|
|
if self._tool_schemas:
|
|
schemas_path = out_dir / "ai_lsc_tool_schemas.json"
|
|
schemas_path.write_text(
|
|
json.dumps(self._tool_schemas, indent=2, ensure_ascii=False),
|
|
encoding="utf-8",
|
|
)
|
|
written["ai_lsc_tool_schemas.json"] = str(schemas_path)
|
|
|
|
logger.info("Saved LibreChat config files to %s", out_dir)
|
|
return written
|