ai-lsc/src/ai_lsc/agents/librechat_config.py

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