Localize the web console and persist custom model and capability settings. Use OpenAI Responses exclusively and move runtime limits into environment configuration. Bootstrap uv and the virtual environment on Windows, support optional China mirrors, and package source distributions without environments, build output, or credentials. Update regression tests and repository quality checks for the new behavior.
151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from artemis.config.settings import settings
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ModelService:
|
|
"""Service handling agent architecture profile detection and metadata formatting."""
|
|
|
|
# parse_llm_config re-reads and re-validates the config file on every call;
|
|
# /api/sessions calls this once per session row, so cache (provider, model_id)
|
|
# for a short TTL. Config edits take effect within _LLM_INFO_TTL seconds.
|
|
_LLM_INFO_TTL = settings.ARTEMIS_MODEL_CACHE_TTL_SECONDS
|
|
_llm_info_cache: tuple[float, str, str] | None = None
|
|
|
|
@classmethod
|
|
def _get_llm_provider_and_model(cls) -> tuple[str, str]:
|
|
cached = cls._llm_info_cache
|
|
now = time.monotonic()
|
|
if cached and now - cached[0] < cls._LLM_INFO_TTL:
|
|
return cached[1], cached[2]
|
|
provider = "openai"
|
|
model_id = settings.OPENAI_MODEL
|
|
try:
|
|
from artemis.config import parse_llm_config
|
|
|
|
llm_cfg = parse_llm_config()
|
|
if llm_cfg and llm_cfg.operator:
|
|
provider = str(llm_cfg.operator.provider)
|
|
model_id = str(llm_cfg.operator.model)
|
|
elif llm_cfg and llm_cfg.default:
|
|
provider = str(llm_cfg.default.provider)
|
|
model_id = str(llm_cfg.default.model)
|
|
except Exception as exc:
|
|
# Unreadable/invalid LLM config: fall back to display defaults
|
|
# (cached for the TTL, so this logs at most once per window).
|
|
logger.warning("Could not resolve LLM config for display: %s", exc)
|
|
cls._llm_info_cache = (now, provider, model_id)
|
|
return provider, model_id
|
|
|
|
@classmethod
|
|
def get_active_model_info(cls, profile: str | None = None) -> dict[str, str]:
|
|
"""Return active architecture and underlying LLM model configuration."""
|
|
# 1. Determine underlying LLM model and provider from config (cached)
|
|
provider, model_id = cls._get_llm_provider_and_model()
|
|
|
|
# 2. Determine agent architecture name (Flash vs Pro)
|
|
arch_name = "Flash"
|
|
if profile:
|
|
p_lower = str(profile).lower()
|
|
if "pro" in p_lower:
|
|
arch_name = "Pro"
|
|
elif "flash" in p_lower:
|
|
arch_name = "Flash"
|
|
else:
|
|
arch_name = str(profile).capitalize()
|
|
|
|
return {
|
|
"name": arch_name,
|
|
"id": model_id,
|
|
"provider": provider,
|
|
"architecture": f"ARTEMIS {arch_name}",
|
|
}
|
|
|
|
@staticmethod
|
|
def resolve_session_profile(
|
|
row_dict: dict[str, Any],
|
|
llm_trace_payloads: list[str] | None = None,
|
|
running_profile: str | None = None,
|
|
agent_names: list[str] | None = None,
|
|
) -> str | None:
|
|
"""Resolve the Artemis agent architecture profile ('flash' or 'pro') for a session."""
|
|
sess_profile = None
|
|
d_info_raw = row_dict.get("device_info")
|
|
if d_info_raw:
|
|
try:
|
|
d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw
|
|
if isinstance(d_info, dict):
|
|
p = d_info.get("profile")
|
|
if p:
|
|
p_str = str(p).lower()
|
|
if "pro" in p_str:
|
|
return "pro"
|
|
elif "flash" in p_str:
|
|
return "flash"
|
|
except (ValueError, TypeError):
|
|
# Malformed device_info JSON: try the other profile sources.
|
|
pass
|
|
|
|
if (row_dict.get("status") == "running") and running_profile:
|
|
p_str = str(running_profile).lower()
|
|
if "pro" in p_str:
|
|
return "pro"
|
|
elif "flash" in p_str:
|
|
return "flash"
|
|
|
|
# Check Agent/Trace names - the most definitive indicator of architecture
|
|
if agent_names:
|
|
agent_names_lower = [str(a).lower() for a in agent_names]
|
|
if any(
|
|
name in ("planner", "validator", "summarizer", "operator", "checker", "diagnoser")
|
|
for name in agent_names_lower
|
|
):
|
|
return "pro"
|
|
if any("flashrunner" in name for name in agent_names_lower):
|
|
return "flash"
|
|
|
|
# Check LLM traces for agent name
|
|
if llm_trace_payloads:
|
|
for tr_payload in llm_trace_payloads:
|
|
if tr_payload:
|
|
try:
|
|
p_obj = (
|
|
json.loads(tr_payload) if isinstance(tr_payload, str) else tr_payload
|
|
)
|
|
if isinstance(p_obj, dict):
|
|
agent_name = str(p_obj.get("agent") or p_obj.get("name") or "").lower()
|
|
if "flashrunner" in agent_name:
|
|
return "flash"
|
|
if any(
|
|
x in agent_name
|
|
for x in ("planner", "validator", "operator", "checker")
|
|
):
|
|
return "pro"
|
|
except (ValueError, TypeError):
|
|
# Malformed trace payload JSON: skip this trace.
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
model_service = ModelService()
|