251 lines
8.4 KiB
Python
251 lines
8.4 KiB
Python
import logging
|
|
import os
|
|
from logging.handlers import TimedRotatingFileHandler
|
|
from pathlib import Path
|
|
|
|
import platformdirs
|
|
from dotenv import load_dotenv
|
|
from langchain_anthropic import ChatAnthropic
|
|
from langchain_openai import ChatOpenAI
|
|
from posthog import Posthog
|
|
from posthog.ai.langchain import CallbackHandler
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
|
|
def _get_log_path() -> Path:
|
|
# Allow explicit override
|
|
if os.environ["STIRLING_LOG_PATH"]:
|
|
return Path(os.environ["STIRLING_LOG_PATH"])
|
|
|
|
# Check if running in Tauri desktop mode
|
|
is_tauri = os.environ["STIRLING_PDF_TAURI_MODE"].lower() == "true"
|
|
|
|
if is_tauri:
|
|
# Use OS-native log directory via platformdirs
|
|
# On Mac: ~/Library/Logs/Stirling-PDF/
|
|
# On Windows: %LOCALAPPDATA%/Stirling-PDF/Logs/
|
|
# On Linux: ~/.local/state/Stirling-PDF/log/
|
|
return Path(platformdirs.user_log_dir("Stirling-PDF"))
|
|
|
|
# Server/Docker mode: ./logs/
|
|
return Path("./logs")
|
|
|
|
|
|
LOG_PATH = _get_log_path()
|
|
LOG_PATH.mkdir(parents=True, exist_ok=True)
|
|
LOG_FILE = LOG_PATH / "docgen.log"
|
|
|
|
# Create formatters
|
|
console_formatter = logging.Formatter(
|
|
"%(asctime)s [%(thread)d] %(levelname)-5s %(name)s - %(message)s", datefmt="%H:%M:%S.%f"
|
|
)
|
|
file_formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s [%(thread)d] %(message)s")
|
|
|
|
# Configure root logger
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.INFO)
|
|
|
|
# Console handler
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(logging.INFO)
|
|
console_handler.setFormatter(console_formatter)
|
|
root_logger.addHandler(console_handler)
|
|
|
|
# File handler with daily rotation, keeping 14 days
|
|
file_handler = TimedRotatingFileHandler(
|
|
LOG_FILE,
|
|
when="midnight",
|
|
interval=1,
|
|
backupCount=14,
|
|
encoding="utf-8",
|
|
)
|
|
file_handler.setLevel(logging.INFO)
|
|
file_handler.setFormatter(file_formatter)
|
|
root_logger.addHandler(file_handler)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Logging to: {LOG_FILE}")
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
|
|
ASSETS_DIR = os.path.join(OUTPUT_DIR, "assets")
|
|
DATA_DIR = os.path.join(BASE_DIR, "data")
|
|
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
|
|
TEMPLATE_DB_PATH = os.path.join(DATA_DIR, "user_templates.json")
|
|
VERSIONS_DB_PATH = os.path.join(DATA_DIR, "versions.json")
|
|
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
os.makedirs(ASSETS_DIR, exist_ok=True)
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
|
|
|
OPENAI_API_KEY = os.environ["STIRLING_OPENAI_API_KEY"]
|
|
OPENAI_BASE_URL = os.environ["STIRLING_OPENAI_BASE_URL"]
|
|
ANTHROPIC_API_KEY = os.environ["STIRLING_ANTHROPIC_API_KEY"]
|
|
JAVA_BACKEND_URL = os.environ["STIRLING_JAVA_BACKEND_URL"]
|
|
JAVA_BACKEND_API_KEY = os.environ["STIRLING_JAVA_BACKEND_API_KEY"]
|
|
JAVA_REQUEST_TIMEOUT_SECONDS = float(os.environ["STIRLING_JAVA_REQUEST_TIMEOUT_SECONDS"])
|
|
|
|
if not OPENAI_API_KEY and not ANTHROPIC_API_KEY:
|
|
raise RuntimeError(
|
|
"Either STIRLING_OPENAI_API_KEY or STIRLING_ANTHROPIC_API_KEY is required to start the AI backend."
|
|
)
|
|
SMART_MODEL = os.environ["STIRLING_SMART_MODEL"]
|
|
FAST_MODEL = os.environ["STIRLING_FAST_MODEL"]
|
|
|
|
# GPT-5 reasoning effort configuration
|
|
# Supported values: minimal, low, medium, high, xhigh
|
|
# - minimal: Fastest (GPT-5 only)
|
|
# - low: Speed focused
|
|
# - medium: Default balance
|
|
# - high: Quality focused
|
|
# - xhigh: Maximum quality (GPT-5.2 Pro/Thinking only)
|
|
SMART_MODEL_REASONING_EFFORT = os.environ["STIRLING_SMART_MODEL_REASONING_EFFORT"]
|
|
FAST_MODEL_REASONING_EFFORT = os.environ["STIRLING_FAST_MODEL_REASONING_EFFORT"]
|
|
|
|
# GPT-5 text verbosity configuration
|
|
# Supported values: minimal, low, medium, high
|
|
# Controls output length and detail level
|
|
SMART_MODEL_TEXT_VERBOSITY = os.environ["STIRLING_SMART_MODEL_TEXT_VERBOSITY"]
|
|
FAST_MODEL_TEXT_VERBOSITY = os.environ["STIRLING_FAST_MODEL_TEXT_VERBOSITY"]
|
|
|
|
FLASK_DEBUG = os.environ["STIRLING_FLASK_DEBUG"] == "1"
|
|
STREAMING_ENABLED = os.environ["STIRLING_AI_STREAMING"].lower() not in {"0", "false", "no"}
|
|
if OPENAI_BASE_URL and "ollama" in OPENAI_BASE_URL and not os.environ["STIRLING_AI_STREAMING"]:
|
|
STREAMING_ENABLED = False
|
|
PREVIEW_MAX_INFLIGHT = int(os.environ["STIRLING_AI_PREVIEW_MAX_INFLIGHT"])
|
|
AI_REQUEST_TIMEOUT_SECONDS = float(os.environ["STIRLING_AI_REQUEST_TIMEOUT"])
|
|
AI_RAW_DEBUG = os.environ["STIRLING_AI_RAW_DEBUG"].lower() not in {"", "0", "false", "no"}
|
|
AI_MESSAGES_LOG_PATH = LOG_PATH / "ai_messages"
|
|
AI_MESSAGES_LOG_PATH.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def model_max_tokens(model_name: str) -> int:
|
|
"""
|
|
Output token limit for a given model.
|
|
|
|
This is used by a few routes to avoid provider defaults that are too small for
|
|
structured outputs. All values can be overridden via env vars.
|
|
"""
|
|
if os.environ["STIRLING_AI_MAX_TOKENS"]:
|
|
return int(os.environ["STIRLING_AI_MAX_TOKENS"])
|
|
|
|
if model_name == SMART_MODEL:
|
|
return int(os.environ["STIRLING_SMART_MODEL_MAX_TOKENS"])
|
|
if model_name == FAST_MODEL:
|
|
return int(os.environ["STIRLING_FAST_MODEL_MAX_TOKENS"])
|
|
if model_name.startswith("claude"):
|
|
return int(os.environ["STIRLING_CLAUDE_MAX_TOKENS"])
|
|
|
|
return int(os.environ["STIRLING_DEFAULT_MODEL_MAX_TOKENS"])
|
|
|
|
|
|
# PostHog Analytics Configuration
|
|
POSTHOG_API_KEY = os.environ["STIRLING_POSTHOG_API_KEY"]
|
|
if not POSTHOG_API_KEY:
|
|
raise RuntimeError("STIRLING_POSTHOG_API_KEY is required to start the AI backend.")
|
|
POSTHOG_HOST = os.environ["STIRLING_POSTHOG_HOST"]
|
|
|
|
# Initialize PostHog client
|
|
POSTHOG_CLIENT = Posthog(
|
|
project_api_key=POSTHOG_API_KEY,
|
|
host=POSTHOG_HOST,
|
|
)
|
|
logger.info(f"PostHog analytics enabled: host={POSTHOG_HOST}")
|
|
|
|
POSTHOG_CALLBACK = CallbackHandler(client=POSTHOG_CLIENT)
|
|
|
|
|
|
def get_chat_model(
|
|
model_name: str,
|
|
streaming: bool = False,
|
|
max_tokens: int | None = None,
|
|
model_kwargs: dict | None = None,
|
|
callbacks: list | None = None,
|
|
):
|
|
# Add PostHog callback if enabled and not already in callbacks
|
|
if callbacks is None or POSTHOG_CALLBACK not in callbacks:
|
|
callbacks = [POSTHOG_CALLBACK] if callbacks is None else [*callbacks, POSTHOG_CALLBACK]
|
|
|
|
# Check if this is a Claude model
|
|
if model_name.startswith("claude"):
|
|
if not ANTHROPIC_API_KEY:
|
|
raise RuntimeError("ANTHROPIC_API_KEY not set")
|
|
|
|
kwargs = {
|
|
"model": model_name,
|
|
"anthropic_api_key": ANTHROPIC_API_KEY,
|
|
"streaming": streaming,
|
|
}
|
|
if max_tokens is not None:
|
|
kwargs["max_tokens"] = max_tokens
|
|
if callbacks:
|
|
kwargs["callbacks"] = callbacks
|
|
|
|
return ChatAnthropic(**kwargs)
|
|
|
|
# OpenAI/GPT models
|
|
kwargs = {"model": model_name, "api_key": OPENAI_API_KEY, "streaming": streaming}
|
|
if max_tokens is not None:
|
|
kwargs["max_tokens"] = max_tokens
|
|
|
|
# Build GPT-5 specific parameters if not provided
|
|
if model_kwargs is None and model_name.startswith("gpt-5"):
|
|
# Determine which settings to use based on model
|
|
if model_name == SMART_MODEL:
|
|
reasoning_effort = SMART_MODEL_REASONING_EFFORT
|
|
text_verbosity = SMART_MODEL_TEXT_VERBOSITY
|
|
elif model_name == FAST_MODEL:
|
|
reasoning_effort = FAST_MODEL_REASONING_EFFORT
|
|
text_verbosity = FAST_MODEL_TEXT_VERBOSITY
|
|
else:
|
|
# Default for other GPT-5 variants
|
|
reasoning_effort = "medium"
|
|
text_verbosity = "medium"
|
|
|
|
# Pass as explicit parameters instead of model_kwargs to avoid warnings
|
|
kwargs["reasoning"] = {"effort": reasoning_effort}
|
|
kwargs["text"] = {"verbosity": text_verbosity}
|
|
elif model_kwargs:
|
|
# If custom model_kwargs provided, pass them through
|
|
kwargs["model_kwargs"] = model_kwargs
|
|
|
|
if callbacks:
|
|
kwargs["callbacks"] = callbacks
|
|
return ChatOpenAI(**kwargs)
|
|
|
|
|
|
__all__ = [
|
|
"OUTPUT_DIR",
|
|
"ASSETS_DIR",
|
|
"DATA_DIR",
|
|
"TEMPLATE_DIR",
|
|
"TEMPLATE_DB_PATH",
|
|
"VERSIONS_DB_PATH",
|
|
"OPENAI_API_KEY",
|
|
"OPENAI_BASE_URL",
|
|
"JAVA_BACKEND_URL",
|
|
"JAVA_BACKEND_API_KEY",
|
|
"JAVA_REQUEST_TIMEOUT_SECONDS",
|
|
"SMART_MODEL",
|
|
"get_chat_model",
|
|
"FAST_MODEL",
|
|
"SMART_MODEL_REASONING_EFFORT",
|
|
"FAST_MODEL_REASONING_EFFORT",
|
|
"SMART_MODEL_TEXT_VERBOSITY",
|
|
"FAST_MODEL_TEXT_VERBOSITY",
|
|
"FLASK_DEBUG",
|
|
"STREAMING_ENABLED",
|
|
"PREVIEW_MAX_INFLIGHT",
|
|
"AI_REQUEST_TIMEOUT_SECONDS",
|
|
"AI_RAW_DEBUG",
|
|
"AI_MESSAGES_LOG_PATH",
|
|
"POSTHOG_API_KEY",
|
|
"POSTHOG_HOST",
|
|
"POSTHOG_CLIENT",
|
|
"POSTHOG_CALLBACK",
|
|
"model_max_tokens",
|
|
]
|