Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions agent_core/core/models/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
"""

import logging
from typing import Optional
import urllib.request
import json as _json

from openai import OpenAI
from anthropic import Anthropic
from typing import Optional

try:
import boto3 # type: ignore[import]
except ImportError: # pragma: no cover — boto3 is an optional extra
Expand Down Expand Up @@ -53,6 +50,51 @@
_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"


_PROVIDER_DISPLAY = {
"openai": "OpenAI",
"deepseek": "DeepSeek",
"grok": "Grok",
"moonshot": "Moonshot",
"minimax": "MiniMax",
"openrouter": "OpenRouter",
}


def _create_openai_client(
*,
provider: str,
api_key: str,
base_url: Optional[str] = None,
):
"""Create an OpenAI SDK client for OpenAI-compatible providers."""
try:
from openai import OpenAI
except ImportError as exc:
display = _PROVIDER_DISPLAY.get(provider, provider)
raise ImportError(
f"The openai package is required for {display} because CraftBot "
"uses the OpenAI-compatible SDK client for this provider. "
"Install it with the Python that launches CraftBot: "
"`python -m pip install 'openai>=2.0.0'`."
) from exc

if base_url:
return OpenAI(api_key=api_key, base_url=base_url)
return OpenAI(api_key=api_key)


def _create_anthropic_client(*, api_key: str):
try:
from anthropic import Anthropic
except ImportError as exc:
raise ImportError(
"The anthropic package is required for the Anthropic provider. "
"Install it with the Python that launches CraftBot: "
"`python -m pip install 'anthropic>=0.97.0'`."
) from exc
return Anthropic(api_key=api_key)


def _to_openrouter_slug(provider: str, model: str) -> str:
"""Convert a provider-native model ID to its OpenRouter slug."""
if "/" in model:
Expand Down Expand Up @@ -120,7 +162,7 @@ def create(
Returns:
Dictionary with provider context including client instances
"""
# OpenAI-compatible providers that use OpenAI client with a custom base_url
# OpenAI-compatible providers that use chat-completions with a custom base_url.
_OPENAI_COMPAT = {"minimax", "deepseek", "moonshot", "grok", "openrouter"}

if provider not in PROVIDER_CONFIG:
Expand Down Expand Up @@ -175,7 +217,10 @@ def create(
return {
"provider": provider,
"model": model,
"client": OpenAI(api_key=api_key),
"client": _create_openai_client(
provider=provider,
api_key=api_key,
),
"gemini_client": None,
"remote_url": None,
"byteplus": None,
Expand Down Expand Up @@ -215,7 +260,7 @@ def create(
"gemini_client": None,
"remote_url": None,
"byteplus": None,
"anthropic_client": Anthropic(api_key=api_key),
"anthropic_client": _create_anthropic_client(api_key=api_key),
"bedrock_client": None,
"initialized": True,
}
Expand Down Expand Up @@ -273,7 +318,11 @@ def create(
return {
"provider": "openrouter",
"model": or_model,
"client": OpenAI(api_key=or_key, base_url=_OPENROUTER_BASE_URL),
"client": _create_openai_client(
provider="openrouter",
api_key=or_key,
base_url=_OPENROUTER_BASE_URL,
),
"gemini_client": None,
"remote_url": None,
"byteplus": None,
Expand All @@ -290,7 +339,11 @@ def create(
return {
"provider": provider,
"model": model,
"client": OpenAI(api_key=api_key, base_url=resolved_base_url),
"client": _create_openai_client(
provider=provider,
api_key=api_key,
base_url=resolved_base_url,
),
"gemini_client": None,
"remote_url": None,
"byteplus": None,
Expand Down
4 changes: 4 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def _suppress_console_logging_early() -> None:
import argparse
import asyncio

from app.runtime_preflight import ensure_current_runtime_dependencies

ensure_current_runtime_dependencies()

# Register agent_core state provider and config before importing AgentBase
# This ensures shared code can access state via get_state()
from agent_core import StateRegistry, ConfigRegistry
Expand Down
165 changes: 165 additions & 0 deletions app/runtime_preflight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
"""Runtime dependency checks that can run before dependency-heavy imports."""

import json
import os
import subprocess
import sys
from typing import Dict, List, Optional, Tuple


_PREFLIGHT_OK_ENV = "CRAFTBOT_RUNTIME_PREFLIGHT_OK"
_MISSING_SENTINEL = "__CRAFTBOT_MISSING_RUNTIME_IMPORTS__"

RUNTIME_IMPORT_CHECKS = {
# Packages imported during backend startup. Provider SDKs stay deferred so
# DeepSeek/OpenRouter users are not forced to install unrelated SDKs.
"requests": "requests",
"pyyaml": "yaml",
"loguru": "loguru",
"nest-asyncio": "nest_asyncio",
"pymongo": "pymongo",
"tzlocal": "tzlocal",
"aiohttp": "aiohttp",
"chromadb": "chromadb",
"tiktoken": "tiktoken",
"mss": "mss",
"httpx": "httpx",
"websockets": "websockets",
"tenacity": "tenacity",
"gradio_client": "gradio_client",
"python-dotenv": "dotenv",
"scikit-learn": "sklearn",
"watchdog": "watchdog",
"croniter": "croniter",
}


def _runtime_import_script(checks: Dict[str, str]) -> str:
return (
"import importlib\n"
"import json\n"
f"checks = {list(checks.items())!r}\n"
"missing = []\n"
"for package_name, import_name in checks:\n"
" try:\n"
" importlib.import_module(import_name)\n"
" except Exception:\n"
" missing.append(package_name)\n"
f"print({_MISSING_SENTINEL!r} + json.dumps(missing))\n"
)


def _runtime_import_command(
use_conda: bool,
env_name: Optional[str],
checks: Dict[str, str],
conda_command: str,
) -> Tuple[List[str], str]:
script = _runtime_import_script(checks)
if use_conda and env_name:
return (
[
conda_command,
"run",
"-n",
env_name,
"python",
"-c",
script,
],
f"conda environment '{env_name}'",
)
return ([sys.executable, "-c", script], sys.executable)


def find_missing_runtime_dependencies(
*,
use_conda: bool,
env_name: Optional[str],
checks: Optional[Dict[str, str]] = None,
conda_command: str = "conda",
) -> Tuple[List[str], str]:
"""Return missing core imports for the Python runtime that will run the agent."""
if checks is None:
checks = RUNTIME_IMPORT_CHECKS
cmd, runtime_label = _runtime_import_command(
use_conda, env_name, checks, conda_command
)

try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
except Exception:
return list(checks), runtime_label
if result.returncode != 0:
return list(checks), runtime_label

for line in reversed(result.stdout.splitlines()):
if line.startswith(_MISSING_SENTINEL):
try:
return json.loads(line[len(_MISSING_SENTINEL) :]), runtime_label
except json.JSONDecodeError:
return list(checks), runtime_label
return list(checks), runtime_label


def print_missing_runtime_dependencies(
*,
missing: List[str],
runtime_label: str,
use_conda: bool,
env_name: Optional[str],
) -> None:
print("\nError: CraftBot Python dependencies are missing.")
print(f"Runtime checked: {runtime_label}")
print("\nMissing imports:")
for package_name in missing:
print(f" - {package_name}")

print(
"\nThis usually means CraftBot is running with a different Python "
"than the one used during install."
)
print("\nFix:")
if use_conda and env_name:
print(" python install.py --conda")
print(f" conda run -n {env_name} python run.py")
else:
print(f" {sys.executable} install.py")
print(f" {sys.executable} run.py")
print("\nIf you installed CraftBot with another Python, start it with that Python.")


def ensure_runtime_dependencies(
*,
use_conda: bool,
env_name: Optional[str],
conda_command: str = "conda",
) -> None:
if getattr(sys, "frozen", False):
return

missing, runtime_label = find_missing_runtime_dependencies(
use_conda=use_conda,
env_name=env_name,
conda_command=conda_command,
)
if missing:
print_missing_runtime_dependencies(
missing=missing,
runtime_label=runtime_label,
use_conda=use_conda,
env_name=env_name,
)
sys.exit(1)


def mark_runtime_dependencies_checked() -> None:
os.environ[_PREFLIGHT_OK_ENV] = "1"


def ensure_current_runtime_dependencies() -> None:
"""Check imports for direct app.main usage with the current interpreter."""
if os.environ.get(_PREFLIGHT_OK_ENV) == "1":
return
ensure_runtime_dependencies(use_conda=False, env_name=None)
Loading