Add dual-prefix AI command

This commit is contained in:
Codex
2026-05-14 13:24:47 +02:00
parent 27b2760079
commit 2a731600e0
16 changed files with 607 additions and 16 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import json
import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@@ -28,12 +28,45 @@ class AccountConfig:
hide_password: bool = False
@dataclass(slots=True)
class AIProviderConfig:
name: str
api_key: str
base_url: str
timeout: float | None = None
@dataclass(slots=True)
class AIModelConfig:
alias: str
provider: str
model: str
description: str | None = None
@dataclass(slots=True)
class AIPromptConfig:
root: Path
system_file: str = "system.md"
parts_dir: str = "parts"
@dataclass(slots=True)
class AIConfig:
default_model: str
context_messages: int
prompt: AIPromptConfig
providers: dict[str, AIProviderConfig] = field(default_factory=dict)
models: dict[str, AIModelConfig] = field(default_factory=dict)
@dataclass(slots=True)
class AppConfig:
api_id: int | None
api_hash: str | None
session_dir: Path
accounts: list[AccountConfig]
ai: AIConfig | None = None
def _read_json_file(path: Path) -> dict[str, Any]:
@@ -47,6 +80,21 @@ def _parse_int(value: Any, *, field_name: str) -> int:
raise ValueError(f"{field_name} must be an integer") from exc
def _parse_float(value: Any, *, field_name: str) -> float:
try:
return float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{field_name} must be a number") from exc
def _parse_mapping(value: Any, *, field_name: str) -> dict[str, Any]:
if value is None:
return {}
if not isinstance(value, dict):
raise ValueError(f"{field_name} must be an object")
return value
def _parse_accounts(raw_accounts: Any, *, default_api_id: int | None, default_api_hash: str | None, default_session_dir: Path) -> list[AccountConfig]:
if raw_accounts is None:
return []
@@ -112,6 +160,90 @@ def _parse_accounts(raw_accounts: Any, *, default_api_id: int | None, default_ap
return accounts
def _parse_ai_config(raw_ai: Any, *, root: Path) -> AIConfig | None:
if raw_ai is None:
return None
if not isinstance(raw_ai, dict):
raise ValueError("ai must be an object")
prompt_raw = _parse_mapping(raw_ai.get("prompt"), field_name="ai.prompt")
prompt_root = Path(str(prompt_raw.get("root", "prompts/ai"))).expanduser()
if not prompt_root.is_absolute():
prompt_root = (root / prompt_root).resolve()
prompt = AIPromptConfig(
root=prompt_root,
system_file=str(prompt_raw.get("system_file", "system.md")),
parts_dir=str(prompt_raw.get("parts_dir", "parts")),
)
default_model = str(raw_ai.get("default_model", "")).strip()
if not default_model:
raise ValueError("ai.default_model is required")
context_messages = _parse_int(raw_ai.get("context_messages", 12), field_name="ai.context_messages")
if context_messages < 0:
raise ValueError("ai.context_messages must be >= 0")
providers_raw = _parse_mapping(raw_ai.get("providers"), field_name="ai.providers")
models_raw = _parse_mapping(raw_ai.get("models"), field_name="ai.models")
providers: dict[str, AIProviderConfig] = {}
for provider_name, provider_config in providers_raw.items():
if not isinstance(provider_config, dict):
raise ValueError(f"ai.providers.{provider_name} must be an object")
api_key = str(provider_config.get("api_key", "")).strip()
base_url = str(provider_config.get("base_url", "")).strip()
if not api_key:
raise ValueError(f"ai.providers.{provider_name}.api_key is required")
if not base_url:
raise ValueError(f"ai.providers.{provider_name}.base_url is required")
timeout_value = provider_config.get("timeout")
timeout = None
if timeout_value is not None:
timeout = _parse_float(timeout_value, field_name=f"ai.providers.{provider_name}.timeout")
providers[provider_name] = AIProviderConfig(
name=provider_name,
api_key=api_key,
base_url=base_url,
timeout=timeout,
)
models: dict[str, AIModelConfig] = {}
for alias, model_config in models_raw.items():
if not isinstance(model_config, dict):
raise ValueError(f"ai.models.{alias} must be an object")
provider = str(model_config.get("provider", "")).strip()
model = str(model_config.get("model", "")).strip()
if not provider:
raise ValueError(f"ai.models.{alias}.provider is required")
if not model:
raise ValueError(f"ai.models.{alias}.model is required")
description = model_config.get("description")
models[alias] = AIModelConfig(
alias=alias,
provider=provider,
model=model,
description=str(description).strip() if description is not None else None,
)
if default_model not in models:
raise ValueError("ai.default_model must reference one of ai.models")
for alias, model_config in models.items():
if model_config.provider not in providers:
raise ValueError(
f"ai.models.{alias}.provider references unknown provider: {model_config.provider}"
)
return AIConfig(
default_model=default_model,
context_messages=context_messages,
prompt=prompt,
providers=providers,
models=models,
)
def _accounts_from_env() -> list[dict[str, Any]] | None:
raw_accounts = os.environ.get("ACCOUNTS_JSON")
if raw_accounts:
@@ -154,6 +286,7 @@ def load_app_config(root: Path) -> AppConfig:
env_api_id = os.environ.get("TG_API_ID")
env_api_hash = os.environ.get("TG_API_HASH")
env_session_dir = os.environ.get("SESSION_DIR")
env_ai_config = os.environ.get("AI_CONFIG")
if env_api_id is not None:
raw["api_id"] = env_api_id
@@ -161,6 +294,8 @@ def load_app_config(root: Path) -> AppConfig:
raw["api_hash"] = env_api_hash
if env_session_dir is not None:
raw["session_dir"] = env_session_dir
if env_ai_config:
raw["ai"] = json.loads(env_ai_config)
env_accounts = _accounts_from_env()
if env_accounts is not None:
@@ -184,6 +319,8 @@ def load_app_config(root: Path) -> AppConfig:
default_session_dir=session_dir,
)
ai = _parse_ai_config(raw.get("ai"), root=root)
if not accounts:
raise ValueError("At least one account must be configured")
@@ -202,4 +339,5 @@ def load_app_config(root: Path) -> AppConfig:
api_hash=api_hash,
session_dir=session_dir,
accounts=accounts,
ai=ai,
)