Release/v0.1.4 (#266)
* feat(middleware): reposition code interpreter middleware in the stack * feat(models): add qwen3.7-plus model entry and update context window comment * feat(models): add qwen3.7-max and qwen3.7-plus model entries for DashScope * feat(auxiliary): implement auxiliary model support for background tasks and tool selection - Added auxiliary model configuration to EvoScientistConfig. - Introduced _ensure_auxiliary_chat_model function to manage auxiliary model instances. - Updated onboarding steps to include auxiliary model selection. - Modified middleware to route tool selection to the auxiliary model when applicable. - Enhanced tests to cover auxiliary model functionality and configuration. * feat(steps): update UI backend selection options and descriptions * Refactor code structure for improved readability and maintainability * feat(patches): implement OpenRouter response reasoning item stripping to prevent multi-turn errors * feat: update version to v0.1.4 in badges, README, and pyproject.toml; adjust skill counts in steps.py * feat(config): add auxiliary model and provider environment variables to test setup
This commit is contained in:
@@ -5,5 +5,5 @@
|
||||
<rect x="54" y="5" width="62" height="24" rx="6" fill="#1565c0"/>
|
||||
<text x="85" y="22" text-anchor="middle"
|
||||
font-family="Inter, -apple-system, system-ui, sans-serif"
|
||||
font-size="13" font-weight="700" fill="#ffffff">v0.1.3</text>
|
||||
font-size="13" font-weight="700" fill="#ffffff">v0.1.4</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 555 B |
@@ -5,5 +5,5 @@
|
||||
<rect x="54" y="5" width="62" height="24" rx="6" fill="#2563eb"/>
|
||||
<text x="85" y="22" text-anchor="middle"
|
||||
font-family="Inter, -apple-system, system-ui, sans-serif"
|
||||
font-size="13" font-weight="700" fill="#ffffff">v0.1.3</text>
|
||||
font-size="13" font-weight="700" fill="#ffffff">v0.1.4</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 555 B |
@@ -59,6 +59,13 @@ _chat_model = None
|
||||
# /model switch to lag one step (see issue #179).
|
||||
_chat_model_key: tuple[str | None, str | None] | None = None
|
||||
|
||||
# Auxiliary model for background/helper LLM calls (memory workers + main-agent
|
||||
# tool selector). Cached separately from the main model; falls back to the main
|
||||
# instance when the auxiliary_* config fields are empty (see
|
||||
# _ensure_auxiliary_chat_model).
|
||||
_auxiliary_chat_model = None
|
||||
_auxiliary_chat_model_key: tuple[str | None, str | None] | None = None
|
||||
|
||||
# Cache MCP tools by the effective config signature to avoid reconnecting
|
||||
# to MCP servers on every `/new` when config is unchanged.
|
||||
_MCP_TOOLS_CACHE_KEY: str | None = None
|
||||
@@ -124,6 +131,32 @@ def _ensure_chat_model():
|
||||
return _chat_model
|
||||
|
||||
|
||||
def _ensure_auxiliary_chat_model():
|
||||
"""Return the auxiliary chat model for background/helper LLM calls.
|
||||
|
||||
Resolves ``(cfg.auxiliary_model or cfg.model, cfg.auxiliary_provider or
|
||||
cfg.provider)``. When the auxiliary fields are empty — or resolve to the same
|
||||
``(model, provider)`` pair as the main model — returns the main
|
||||
``_ensure_chat_model()`` instance directly, so no second client is built.
|
||||
Otherwise it is cached separately under its own key. Onboard sets the
|
||||
provider alongside the model, so the ``or cfg.provider`` fallback only
|
||||
matters for a model set without an explicit auxiliary provider.
|
||||
"""
|
||||
global _auxiliary_chat_model, _auxiliary_chat_model_key
|
||||
from .llm import get_chat_model
|
||||
|
||||
cfg = _ensure_config()
|
||||
aux_model = cfg.auxiliary_model or cfg.model
|
||||
aux_provider = cfg.auxiliary_provider or cfg.provider
|
||||
if (aux_model, aux_provider) == (cfg.model, cfg.provider):
|
||||
return _ensure_chat_model()
|
||||
key = (aux_model, aux_provider)
|
||||
if _auxiliary_chat_model is None or _auxiliary_chat_model_key != key:
|
||||
_auxiliary_chat_model = get_chat_model(model=aux_model, provider=aux_provider)
|
||||
_auxiliary_chat_model_key = key
|
||||
return _auxiliary_chat_model
|
||||
|
||||
|
||||
def set_chat_model(model: str, provider: str | None = None):
|
||||
"""Replace the cached chat model with a new one.
|
||||
|
||||
@@ -136,6 +169,12 @@ def set_chat_model(model: str, provider: str | None = None):
|
||||
"""
|
||||
from .llm import get_chat_model
|
||||
|
||||
# Invalidate the auxiliary cache too: when auxiliary_* is empty it mirrors
|
||||
# the main model, so a /model switch must let it re-resolve to the new main.
|
||||
global _auxiliary_chat_model, _auxiliary_chat_model_key
|
||||
_auxiliary_chat_model = None
|
||||
_auxiliary_chat_model_key = None
|
||||
|
||||
key = (model, provider)
|
||||
if _chat_model is None or _chat_model_key != key:
|
||||
_replace_chat_model(get_chat_model(model=model, provider=provider), key)
|
||||
@@ -585,13 +624,26 @@ def _get_default_middleware(
|
||||
MemoryObservationTarget.AGENT
|
||||
),
|
||||
)
|
||||
# Main-agent tool selection may use the auxiliary model; async sub-agents
|
||||
# keep the main model (they do real work, not a one-off helper call).
|
||||
# context_editing stays on the main model — its model only sizes the
|
||||
# context-window trigger for the main agent's own history.
|
||||
tool_selector_model = (
|
||||
model if for_async_subagent else _ensure_auxiliary_chat_model()
|
||||
)
|
||||
mw = [
|
||||
ConfigurableModelMiddleware(),
|
||||
create_context_editing_middleware(model),
|
||||
ModelFallbackMiddleware(),
|
||||
ContextOverflowMapperMiddleware(),
|
||||
ToolErrorHandlerMiddleware(),
|
||||
*create_tool_selector_middleware(model=model),
|
||||
*create_tool_selector_middleware(model=tool_selector_model),
|
||||
# Interpreter prompt must land before runtime/memory context, so this
|
||||
# middleware sits ahead of runtime_context in the stack.
|
||||
create_code_interpreter_middleware(
|
||||
timeout=cfg.code_interpreter_timeout,
|
||||
max_result_chars=cfg.code_interpreter_max_result_chars,
|
||||
),
|
||||
create_runtime_context_middleware(),
|
||||
]
|
||||
if memory_controls.memory_enabled:
|
||||
@@ -624,12 +676,6 @@ def _get_default_middleware(
|
||||
|
||||
mw.append(BackgroundExecutionMiddleware())
|
||||
|
||||
mw.append(
|
||||
create_code_interpreter_middleware(
|
||||
timeout=cfg.code_interpreter_timeout,
|
||||
max_result_chars=cfg.code_interpreter_max_result_chars,
|
||||
)
|
||||
)
|
||||
return mw
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ from .validators import validate_tavily_key
|
||||
|
||||
|
||||
def _step_ui_backend(config: EvoScientistConfig) -> str:
|
||||
"""Step 0: Select UI backend (Textual TUI, Rich CLI, or browser WebUI).
|
||||
"""Step 0: Select UI backend (desktop WebUI, Textual TUI, or Rich CLI).
|
||||
|
||||
Args:
|
||||
config: Current configuration.
|
||||
@@ -52,9 +52,9 @@ def _step_ui_backend(config: EvoScientistConfig) -> str:
|
||||
Selected backend name ("tui", "cli", or "webui").
|
||||
"""
|
||||
choices = [
|
||||
Choice(title="WebUI (desktop interface, beta)", value="webui"),
|
||||
Choice(title="TUI (full-screen interface, recommended)", value="tui"),
|
||||
Choice(title="CLI (classic terminal, lightweight)", value="cli"),
|
||||
Choice(title="WebUI (browser interface, beta)", value="webui"),
|
||||
]
|
||||
|
||||
# Map legacy values to current ones
|
||||
@@ -228,11 +228,20 @@ def _step_webui_port(config: EvoScientistConfig) -> int:
|
||||
return port
|
||||
|
||||
|
||||
def _step_provider(config: EvoScientistConfig) -> str:
|
||||
def _step_provider(
|
||||
config: EvoScientistConfig,
|
||||
*,
|
||||
label: str | None = None,
|
||||
default_value: str | None = None,
|
||||
) -> str:
|
||||
"""Step 1: Select LLM provider.
|
||||
|
||||
Args:
|
||||
config: Current configuration.
|
||||
label: Optional role label (e.g. "co-pilot") to clarify which model this
|
||||
provider is for. When omitted, the generic main-model prompt is used.
|
||||
default_value: Preselect this provider instead of ``config.provider``
|
||||
(e.g. the auxiliary provider when configuring the co-pilot).
|
||||
|
||||
Returns:
|
||||
Selected provider name.
|
||||
@@ -297,12 +306,13 @@ def _step_provider(config: EvoScientistConfig) -> str:
|
||||
),
|
||||
]
|
||||
|
||||
# Set default based on current config
|
||||
# Set default based on current config (or an explicit override).
|
||||
valid_providers = {c.value for c in choices}
|
||||
default = config.provider if config.provider in valid_providers else "anthropic"
|
||||
preferred = default_value or config.provider
|
||||
default = preferred if preferred in valid_providers else "anthropic"
|
||||
|
||||
provider = questionary.select(
|
||||
"Select your LLM provider:",
|
||||
f"Select {label} provider:" if label else "Select your LLM provider:",
|
||||
choices=choices,
|
||||
default=default,
|
||||
style=WIZARD_STYLE,
|
||||
@@ -671,6 +681,8 @@ def _step_model(
|
||||
provider: str,
|
||||
*,
|
||||
ollama_detected_models: list[str] | None = None,
|
||||
label: str | None = None,
|
||||
default_value: str | None = None,
|
||||
) -> str:
|
||||
"""Step 3: Select model for the provider.
|
||||
|
||||
@@ -678,10 +690,16 @@ def _step_model(
|
||||
config: Current configuration.
|
||||
provider: Selected provider name.
|
||||
ollama_detected_models: Model names detected from a live Ollama server.
|
||||
label: Optional role label (e.g. "co-pilot") for the prompt. When omitted,
|
||||
the generic main-model prompt is used.
|
||||
default_value: Preselect this model instead of ``config.model`` (e.g. the
|
||||
auxiliary model when configuring the co-pilot).
|
||||
|
||||
Returns:
|
||||
Selected model name.
|
||||
"""
|
||||
model_prompt = f"Select {label} model:" if label else "Select model:"
|
||||
model_default = default_value or config.model
|
||||
# Ollama: show only what's actually pulled on the server
|
||||
if provider == "ollama":
|
||||
if ollama_detected_models:
|
||||
@@ -692,11 +710,11 @@ def _step_model(
|
||||
choices.append(Choice(title="Type a model name...", value=_CUSTOM_SENTINEL))
|
||||
|
||||
default = ollama_detected_models[0]
|
||||
if config.model in ollama_detected_models:
|
||||
default = config.model
|
||||
if model_default in ollama_detected_models:
|
||||
default = model_default
|
||||
|
||||
selected = questionary.select(
|
||||
"Select model:",
|
||||
model_prompt,
|
||||
choices=choices,
|
||||
default=default,
|
||||
style=WIZARD_STYLE,
|
||||
@@ -742,7 +760,7 @@ def _step_model(
|
||||
style=WIZARD_STYLE,
|
||||
qmark=QMARK,
|
||||
placeholder=FormattedText([("fg:#858585", " e.g. owner/model-name")]),
|
||||
default=config.model or "",
|
||||
default=model_default or "",
|
||||
).ask()
|
||||
if model is None:
|
||||
raise KeyboardInterrupt()
|
||||
@@ -763,14 +781,23 @@ def _step_model(
|
||||
choices.append(Choice(title=f"{name} ({model_id})", value=name))
|
||||
choices.append(Choice(title="Type a model name...", value=_CUSTOM_SENTINEL))
|
||||
|
||||
# Determine default
|
||||
if config.model in provider_models:
|
||||
default = config.model
|
||||
# Determine default. An explicit ``default_value`` override (e.g. a saved
|
||||
# co-pilot model on a re-run) that isn't a registry model is a custom name:
|
||||
# preselect "Type a model name..." and prefill it. A plain ``config.model``
|
||||
# that just isn't in the current provider's list (e.g. the provider was
|
||||
# changed) falls back to the first model, NOT the custom entry.
|
||||
custom_default = (
|
||||
default_value if default_value and default_value not in provider_models else ""
|
||||
)
|
||||
if model_default in provider_models:
|
||||
default = model_default
|
||||
elif custom_default:
|
||||
default = _CUSTOM_SENTINEL
|
||||
else:
|
||||
default = provider_models[0]
|
||||
|
||||
selected = questionary.select(
|
||||
"Select model:",
|
||||
model_prompt,
|
||||
choices=choices,
|
||||
default=default,
|
||||
style=WIZARD_STYLE,
|
||||
@@ -786,6 +813,7 @@ def _step_model(
|
||||
|
||||
model = questionary.text(
|
||||
"Model name:",
|
||||
default=custom_default,
|
||||
style=WIZARD_STYLE,
|
||||
qmark=QMARK,
|
||||
placeholder=FormattedText([("fg:#858585", " e.g. owner/model-name")]),
|
||||
@@ -799,6 +827,40 @@ def _step_model(
|
||||
return model
|
||||
|
||||
|
||||
def _step_auxiliary_enable(config: EvoScientistConfig) -> bool:
|
||||
"""Step 3.25: Choose whether to assemble a co-pilot (auxiliary) model.
|
||||
|
||||
The co-pilot runs background/helper LLM calls — EvoMemory (memory workers)
|
||||
and the main agent's tool selector — so it can be a cheaper/faster model.
|
||||
Returns True when the user picks "Assemble"; the caller then runs the
|
||||
provider/key/model pickers. Returns False to keep the pilot (main model)
|
||||
everywhere.
|
||||
"""
|
||||
console.print(
|
||||
" [dim]A cheaper/faster co-pilot for EvoMemory (memory workers).[/dim]"
|
||||
)
|
||||
choice = questionary.select(
|
||||
"Co-pilot (auxiliary model):",
|
||||
choices=[
|
||||
Choice(
|
||||
title="Skip — single pilot (main model handles everything)",
|
||||
value="skip",
|
||||
),
|
||||
Choice(
|
||||
title="Assemble a co-pilot — separate cheaper/faster model",
|
||||
value="assemble",
|
||||
),
|
||||
],
|
||||
default="assemble" if config.auxiliary_model else "skip",
|
||||
style=WIZARD_STYLE,
|
||||
qmark=QMARK,
|
||||
use_indicator=True,
|
||||
).ask()
|
||||
if choice is None:
|
||||
raise KeyboardInterrupt()
|
||||
return choice == "assemble"
|
||||
|
||||
|
||||
def _step_reasoning_effort(config: EvoScientistConfig) -> str:
|
||||
"""Step 3.5: Configure OpenRouter reasoning effort level.
|
||||
|
||||
@@ -935,18 +997,23 @@ _RECOMMENDED_SKILLS = [
|
||||
},
|
||||
# ── Third-party (K-Dense) ──
|
||||
{
|
||||
"label": "Scientific Skills (147 research & experiment skills, third party by K-Dense)",
|
||||
"label": "Scientific Skills (143 research & experiment skills, third party by K-Dense)",
|
||||
"source": "K-Dense-AI/scientific-agent-skills@skills",
|
||||
},
|
||||
{
|
||||
"label": "Scientific Writer (23 writing, review & presentation skills, third party by K-Dense)",
|
||||
"label": "Scientific Writer (27 writing, review & presentation skills, third party by K-Dense)",
|
||||
"source": "K-Dense-AI/claude-scientific-writer@skills",
|
||||
},
|
||||
# ── Third-party (Orchestra Research) ──
|
||||
{
|
||||
"label": "AI Research Skills (85 skills for training, evaluation, deployment, etc., third party by Orchestra Research)",
|
||||
"label": "AI Research Skills (98 skills for training, evaluation, deployment, etc., third party by Orchestra Research)",
|
||||
"source": "Orchestra-Research/AI-Research-SKILLs",
|
||||
},
|
||||
# ── Third-party (Google DeepMind) ──
|
||||
{
|
||||
"label": "Science Skills (37 genomics, structural-biology & literature skills, third party by Google DeepMind)",
|
||||
"source": "google-deepmind/science-skills@skills",
|
||||
},
|
||||
# ── Third-party (Anthropic) ──
|
||||
{
|
||||
"label": "Anthropic Skills (co-authoring, design, etc., third party by Anthropic)",
|
||||
|
||||
@@ -104,6 +104,21 @@ def _print_header() -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
_SECTION_WIDTH = 53 # ~ the Setup Wizard panel width (not the full terminal)
|
||||
|
||||
|
||||
def _print_section(title: str) -> None:
|
||||
"""Print a compact bold-cyan section divider, e.g. ──── Pilot ────.
|
||||
|
||||
Sized to roughly the Setup Wizard panel width and styled like the section
|
||||
markers used elsewhere in the CLI, rather than a full-width rule.
|
||||
"""
|
||||
label = f" {title} "
|
||||
pad = max(2, _SECTION_WIDTH - len(label))
|
||||
left = pad // 2
|
||||
console.print(f"[bold cyan]{'─' * left}{label}{'─' * (pad - left)}[/bold cyan]")
|
||||
|
||||
|
||||
def _print_step_result(step_name: str, value: str, success: bool = True) -> None:
|
||||
"""Print a completed step result inline.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..settings import (
|
||||
from .channels import _step_channels
|
||||
from .steps import (
|
||||
_step_anthropic_auth_mode,
|
||||
_step_auxiliary_enable,
|
||||
_step_base_url,
|
||||
_step_langgraph_dev_port,
|
||||
_step_mcp_servers,
|
||||
@@ -40,6 +41,7 @@ from .style import (
|
||||
CONFIRM_STYLE,
|
||||
QMARK,
|
||||
_print_header,
|
||||
_print_section,
|
||||
_print_step_skipped,
|
||||
console,
|
||||
)
|
||||
@@ -50,6 +52,7 @@ STEPS = [
|
||||
"Provider",
|
||||
"API Key",
|
||||
"Model",
|
||||
"Auxiliary Model",
|
||||
"Tavily Key",
|
||||
"Workspace",
|
||||
"Thinking",
|
||||
@@ -145,6 +148,7 @@ _SECTION_LABELS: list[tuple[str, str]] = [
|
||||
("port", "LangGraph server port"),
|
||||
("provider", "LLM provider + auth + API key"),
|
||||
("model", "Model + reasoning effort"),
|
||||
("auxiliary_model", "Auxiliary model (optional)"),
|
||||
("tavily", "Tavily search key"),
|
||||
("workspace", "Workspace mode"),
|
||||
("thinking", "Thinking panel"),
|
||||
@@ -453,6 +457,7 @@ def run_onboard(
|
||||
if "provider" in sections_to_run:
|
||||
from .prompter import GoBack
|
||||
|
||||
_print_section("EvoScientist · Pilot (Main model)")
|
||||
_require("provider", "LLM provider")
|
||||
# Provider sub-loop: auth_mode can raise GoBack to re-pick provider.
|
||||
# We snapshot config at the top of each iteration so a GoBack can
|
||||
@@ -665,6 +670,63 @@ def run_onboard(
|
||||
config.reasoning_effort = _step_reasoning_effort(config)
|
||||
_autosave(config)
|
||||
|
||||
if "auxiliary_model" in sections_to_run:
|
||||
_print_section("Co-pilot (Auxiliary model)")
|
||||
if strict:
|
||||
# Optional; never prompt under --non-interactive. Keep
|
||||
# current (default empty = use main model).
|
||||
_print_step_skipped(
|
||||
"Auxiliary Model",
|
||||
"kept current" if config.auxiliary_model else "not set",
|
||||
)
|
||||
elif _step_auxiliary_enable(config):
|
||||
# Assemble: pick provider -> base URL (custom) -> key -> model,
|
||||
# mirroring the main flow's order. Keys/base URLs are stored
|
||||
# per provider, so when the auxiliary provider matches the main
|
||||
# one they're already set and the user just keeps them (Enter).
|
||||
# Ollama needs no key. Re-runs default to the saved auxiliary
|
||||
# provider/model rather than the main ones.
|
||||
aux_provider = _step_provider(
|
||||
config,
|
||||
label="co-pilot",
|
||||
default_value=config.auxiliary_provider,
|
||||
)
|
||||
config.auxiliary_provider = aux_provider
|
||||
if aux_provider == "custom-openai":
|
||||
config.custom_openai_base_url = _step_base_url(
|
||||
config,
|
||||
current_value=config.custom_openai_base_url
|
||||
or os.environ.get("CUSTOM_OPENAI_BASE_URL", ""),
|
||||
)
|
||||
elif aux_provider == "custom-anthropic":
|
||||
config.custom_anthropic_base_url = _step_base_url(
|
||||
config,
|
||||
current_value=config.custom_anthropic_base_url
|
||||
or os.environ.get("CUSTOM_ANTHROPIC_BASE_URL", ""),
|
||||
)
|
||||
elif aux_provider == "minimax":
|
||||
config.minimax_base_url = _step_minimax_region(config)
|
||||
if aux_provider != "ollama":
|
||||
aux_key_attr = _PROVIDER_KEY_ATTR.get(
|
||||
aux_provider, "openai_api_key"
|
||||
)
|
||||
new_aux_key = _step_provider_api_key(
|
||||
config, aux_provider, skip_validation
|
||||
)
|
||||
if new_aux_key is not None:
|
||||
setattr(config, aux_key_attr, new_aux_key)
|
||||
config.auxiliary_model = _step_model(
|
||||
config,
|
||||
aux_provider,
|
||||
label="co-pilot",
|
||||
default_value=config.auxiliary_model,
|
||||
)
|
||||
else:
|
||||
# Skip: single driver — clear any prior auxiliary config.
|
||||
config.auxiliary_provider = ""
|
||||
config.auxiliary_model = ""
|
||||
_autosave(config)
|
||||
|
||||
if "tavily" in sections_to_run:
|
||||
preset_tavily = _preset("tavily_key")
|
||||
if preset_tavily is not None:
|
||||
|
||||
@@ -95,6 +95,8 @@ class EvoScientistConfig:
|
||||
tavily_api_key: Tavily API key for web search.
|
||||
provider: Default LLM provider ('anthropic', 'openai', 'google-genai', or 'nvidia').
|
||||
model: Default model name (short name or full ID).
|
||||
auxiliary_provider: Provider for auxiliary_model (empty = use main provider).
|
||||
auxiliary_model: Model for memory workers + tool selector (empty = use main model).
|
||||
default_mode: Default workspace mode ('daemon' or 'run').
|
||||
default_workdir: Default workspace directory (empty = use current working directory).
|
||||
show_thinking: Whether to show thinking panels in CLI.
|
||||
@@ -129,6 +131,10 @@ class EvoScientistConfig:
|
||||
provider: str = "anthropic"
|
||||
model: str = "claude-sonnet-4-6"
|
||||
model_fallbacks: str = "" # "model:provider,model:provider" fallback chain
|
||||
# Optional auxiliary model for background/helper LLM calls (memory workers +
|
||||
# tool selector). Empty = fall back to the main model/provider.
|
||||
auxiliary_provider: str = "" # empty = use main provider
|
||||
auxiliary_model: str = "" # empty = use main model
|
||||
|
||||
# Async Sub-agent Settings
|
||||
# When True (default), the EvoSci CLI auto-starts a langgraph dev subprocess
|
||||
@@ -622,6 +628,8 @@ _ENV_MAPPINGS = {
|
||||
"ui_backend": "EVOSCIENTIST_UI_BACKEND",
|
||||
"log_level": "EVOSCIENTIST_LOG_LEVEL",
|
||||
"model_fallbacks": "EVOSCIENTIST_MODEL_FALLBACKS",
|
||||
"auxiliary_provider": "EVOSCIENTIST_AUXILIARY_PROVIDER",
|
||||
"auxiliary_model": "EVOSCIENTIST_AUXILIARY_MODEL",
|
||||
"reasoning_effort": "EVOSCIENTIST_REASONING_EFFORT",
|
||||
"channel_debug_tracing": "EVOSCIENTIST_CHANNEL_DEBUG_TRACING",
|
||||
"ccproxy_port": "EVOSCIENTIST_CCPROXY_PORT",
|
||||
|
||||
@@ -17,8 +17,9 @@ _KNOWN_MODEL_CONTEXT_WINDOWS: dict[str, int] = {
|
||||
# Qwen 3.6 open-source variants — exceptions to the ``qwen3.6`` family.
|
||||
"qwen3.6-27b": 262_000,
|
||||
"qwen3.6-35b-a3b": 262_000,
|
||||
# Qwen 3.7 Max — closed-source flagship (1M).
|
||||
# Qwen 3.7 closed-source tiers — Max flagship and Plus (1M).
|
||||
"qwen3.7-max": 1_000_000,
|
||||
"qwen3.7-plus": 1_000_000,
|
||||
# xAI Grok — per-model windows (build-0.1: 256K, 4.3: 1M).
|
||||
"grok-build-0.1": 256_000,
|
||||
"grok-4.3": 1_000_000,
|
||||
|
||||
@@ -20,6 +20,7 @@ from .patches import (
|
||||
_patch_ccproxy_system_to_developer,
|
||||
_patch_deepseek_reasoning_passback,
|
||||
_patch_openai_compat_content,
|
||||
_patch_openrouter_strip_responses_reasoning,
|
||||
)
|
||||
|
||||
_MINIMAX_ANTHROPIC_BASE_URL = "https://api.minimaxi.com/anthropic"
|
||||
@@ -149,6 +150,7 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
|
||||
("grok-build-0.1", "x-ai/grok-build-0.1", "openrouter"),
|
||||
("grok-4.3", "x-ai/grok-4.3", "openrouter"),
|
||||
("qwen3.7-max", "qwen/qwen3.7-max", "openrouter"),
|
||||
("qwen3.7-plus", "qwen/qwen3.7-plus", "openrouter"),
|
||||
("qwen3.6-flash", "qwen/qwen3.6-flash", "openrouter"),
|
||||
("qwen3.5-122b", "qwen/qwen3.5-122b-a10b", "openrouter"),
|
||||
("deepseek-v4-pro", "deepseek/deepseek-v4-pro", "openrouter"),
|
||||
@@ -174,11 +176,21 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
|
||||
("doubao-1.5-pro", "doubao-1.5-pro-256k", "volcengine"),
|
||||
("doubao-1.5-thinking-pro", "doubao-1.5-thinking-pro", "volcengine"),
|
||||
# DashScope Coding Plan (阿里云代码计划 — subscription sk-sp-* endpoint)
|
||||
("qwen3.7-max", "qwen3.7-max", "dashscope-code"),
|
||||
("qwen3.7-plus", "qwen3.7-plus", "dashscope-code"),
|
||||
("qwen3.6-max", "qwen3.6-max-preview", "dashscope-code"),
|
||||
("qwen3.6-plus", "qwen3.6-plus", "dashscope-code"),
|
||||
("qwen3.6-flash", "qwen3.6-flash", "dashscope-code"),
|
||||
("qwen3-coder", "qwen3-coder-plus", "dashscope-code"),
|
||||
("qwen3-coder-next", "qwen3-coder-next", "dashscope-code"),
|
||||
("qwen3-max", "qwen3-max", "dashscope-code"),
|
||||
("qwen3.5-plus", "qwen3.5-plus", "dashscope-code"),
|
||||
# DashScope (阿里云 — Qwen models, default for simple lookups)
|
||||
("qwen3.7-max", "qwen3.7-max", "dashscope"),
|
||||
("qwen3.7-plus", "qwen3.7-plus", "dashscope"),
|
||||
("qwen3.6-max", "qwen3.6-max-preview", "dashscope"),
|
||||
("qwen3.6-plus", "qwen3.6-plus", "dashscope"),
|
||||
("qwen3.6-flash", "qwen3.6-flash", "dashscope"),
|
||||
("qwen3-coder", "qwen3-coder-plus", "dashscope"),
|
||||
("qwen3-235b", "qwen3-235b-a22b", "dashscope"),
|
||||
("qwen-max", "qwen-max", "dashscope"),
|
||||
@@ -403,9 +415,15 @@ def get_chat_model(
|
||||
api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
# Enable reasoning; disable summary to avoid multi-turn schema errors.
|
||||
# Reasoning via `effort` + `summary: "auto"` so a readable reasoning
|
||||
# summary is returned for display. OpenAI-Responses also emits encrypted
|
||||
# reasoning items (`rs_*` id) that can't be replayed on multi-turn
|
||||
# passback (OpenRouter's `/responses` beta is stateless, store=false —
|
||||
# "Item with id 'rs_...' not found"); the patch strips them on passback,
|
||||
# so enabling `summary` is safe. See langchain-ai/langchain#37777.
|
||||
effort = os.environ.get("EVOSCIENTIST_REASONING_EFFORT", "").strip() or "high"
|
||||
kwargs.setdefault("reasoning", {"effort": effort, "summary": "disabled"})
|
||||
kwargs.setdefault("reasoning", {"effort": effort, "summary": "auto"})
|
||||
_patch_openrouter_strip_responses_reasoning()
|
||||
|
||||
# Anthropic-routed providers → route through Anthropic provider with base_url
|
||||
elif provider in _ANTHROPIC_ROUTED_PROVIDERS:
|
||||
|
||||
@@ -14,6 +14,9 @@ Patches:
|
||||
- _patch_deepseek_reasoning_passback: re-inject reasoning_content into
|
||||
outgoing DeepSeek assistant messages for thinking-mode multi-turn /
|
||||
tool_use scenarios
|
||||
- _patch_openrouter_strip_responses_reasoning: drop OpenAI-Responses
|
||||
encrypted reasoning items (rs_* id) from outgoing OpenRouter messages
|
||||
(store=false → "Item with id rs_... not found")
|
||||
|
||||
Utilities:
|
||||
- _is_ccproxy_codex: detect ccproxy Codex OAuth adapter
|
||||
@@ -726,6 +729,60 @@ def _patch_openai_capture_reasoning_content() -> None:
|
||||
_patch_openai_capture_reasoning_content()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch (lazy, OpenRouter only): strip OpenAI-Responses encrypted reasoning
|
||||
# items from outgoing assistant messages.
|
||||
#
|
||||
# OpenRouter's `/responses` beta is stateless — it does not propagate
|
||||
# `store=true` / `previous_response_id` upstream. So when a prior turn's
|
||||
# assistant message carries a reasoning item with an `rs_*` id (the encrypted
|
||||
# Responses reasoning block), replaying it on the next turn fails with HTTP 400:
|
||||
# "Item with id 'rs_...' not found. Items are not persisted when `store` is
|
||||
# set to false. ... remove this item from your input."
|
||||
# (observed against both Azure and OpenAI upstreams — so deepagents'
|
||||
# azure-ignore is not sufficient). The only robust fix is to drop these items
|
||||
# on passback, exactly as the upstream error instructs. Reasoning DISPLAY is
|
||||
# unaffected: it happens when the item is generated, not on passback.
|
||||
# Related: langchain-ai/langchain#37777.
|
||||
# ---------------------------------------------------------------------------
|
||||
_openrouter_reasoning_strip_patched = False
|
||||
|
||||
|
||||
def _is_responses_reasoning_item(entry: Any) -> bool:
|
||||
"""True for an OpenAI-Responses encrypted reasoning item (`rs_` id / data)."""
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
return str(entry.get("id") or "").startswith("rs_") or bool(entry.get("data"))
|
||||
|
||||
|
||||
def _patch_openrouter_strip_responses_reasoning() -> None:
|
||||
global _openrouter_reasoning_strip_patched
|
||||
if _openrouter_reasoning_strip_patched:
|
||||
return
|
||||
try:
|
||||
import langchain_openrouter.chat_models as _mod
|
||||
|
||||
_orig = _mod._convert_message_to_dict
|
||||
|
||||
def _patched(message: Any) -> Any:
|
||||
result = _orig(message)
|
||||
details = (
|
||||
result.get("reasoning_details") if isinstance(result, dict) else None
|
||||
)
|
||||
if isinstance(details, list):
|
||||
kept = [e for e in details if not _is_responses_reasoning_item(e)]
|
||||
if kept:
|
||||
result["reasoning_details"] = kept
|
||||
else:
|
||||
result.pop("reasoning_details", None)
|
||||
return result
|
||||
|
||||
_mod._convert_message_to_dict = _patched
|
||||
_openrouter_reasoning_strip_patched = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch: DeepSeek thinking mode requires reasoning_content to be passed back
|
||||
# in all assistant messages for multi-turn + tool_use scenarios.
|
||||
|
||||
@@ -706,11 +706,13 @@ def _build_memory_worker_agent(
|
||||
"""Create a background memory worker agent for one lifecycle hook."""
|
||||
from deepagents import create_deep_agent
|
||||
|
||||
from ..EvoScientist import _ensure_chat_model
|
||||
from ..EvoScientist import _ensure_auxiliary_chat_model
|
||||
|
||||
agent = create_deep_agent(
|
||||
name=role.worker_agent_name,
|
||||
model=_ensure_chat_model(),
|
||||
# Memory workers are background helper agents — use the auxiliary model
|
||||
# (falls back to the main model when auxiliary_* is unset).
|
||||
model=_ensure_auxiliary_chat_model(),
|
||||
system_prompt=system_prompt,
|
||||
tools=[],
|
||||
backend=_build_memory_worker_backend(
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://pypi.org/project/EvoScientist/"><picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-dark.svg">
|
||||
<img alt="PyPI v0.1.3" src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg" height="28">
|
||||
<img alt="PyPI v0.1.4" src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg" height="28">
|
||||
</picture></a><a href="https://EvoScientist.github.io/"><picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-website-light.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-website-dark.svg">
|
||||
@@ -109,7 +109,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
|
||||
|
||||
## ✨ Features
|
||||
- **🤖 Multi-Agent Team** — 6 sub-agents (plan, research, code, debug, analyze, write) working in concert.
|
||||
- **🧠 Persistent Memory** — Context, preferences, and findings survive across sessions.
|
||||
- **🧠 Self-Evolving Memory** — User profile and observations auto-distilled each turn, growing across sessions.
|
||||
- **🌐 Multi-Provider** — Anthropic, OpenAI, Google, MiniMax, NVIDIA — one config to switch.
|
||||
- **📱 Multi-Channel** — CLI as the hub; Telegram, Slack, Feishu, WeChat, and more — one agent session.
|
||||
- **🖥️ Browser WebUI (beta)** — Workspace-panel web app, one terminal via `--ui webui`.
|
||||
@@ -136,6 +136,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
|
||||
<details>
|
||||
<summary>📦 Release Highlights — version changelog</summary>
|
||||
|
||||
- **[07 Jun 2026]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — Auxiliary model for background tasks & tool selection, observation-memory lifecycle, Qwen3.7-Max/Plus (DashScope), UI-backend selection, plus an OpenRouter multi-turn reasoning fix.
|
||||
- **[03 Jun 2026]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — Multimodal handling (image + PDF/doc flatten/hoisting, text-only model fallback), runtime-context middleware, memory middleware → profile files with stream timeline narration, textual CJK-input fix.
|
||||
- **[02 Jun 2026]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — Browser WebUI mode (beta), `EvoSci deploy` standalone LangGraph server, default model → claude-sonnet-4-6, MiniMax M3, plus sandbox-timeout and async-notifier channel-routing fixes.
|
||||
- **[19 May 2026]** **[v0.1.1](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.1)** — deepagents 0.6.2 DeltaChannel upgrade, tier-aware skill mounts, status & elapsed-time bar, QQ inline buttons.
|
||||
|
||||
+3
-2
@@ -15,7 +15,7 @@
|
||||
<a href="https://pypi.org/project/EvoScientist/"><picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-dark.svg">
|
||||
<img alt="PyPI v0.1.3" src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg" height="28">
|
||||
<img alt="PyPI v0.1.4" src="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-pypi-light.svg" height="28">
|
||||
</picture></a><a href="https://EvoScientist.github.io/"><picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-website-light.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/EvoScientist/EvoScientist/main/.github/assets/badge-website-dark.svg">
|
||||
@@ -117,7 +117,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
|
||||
## ✨ 特性
|
||||
|
||||
- **🤖 多智能体协作** — 6 个子智能体(规划、调研、编码、调试、分析、写作)协同工作。
|
||||
- **🧠 持久化记忆** — 上下文、偏好和研究发现跨会话保持。
|
||||
- **🧠 自进化记忆** — 用户画像与观察记录每轮自动提炼,跨会话持续进化。
|
||||
- **🌐 多模型供应商** — Anthropic、OpenAI、Google、MiniMax、NVIDIA——一处配置,随时切换。
|
||||
- **📱 多渠道接入** — CLI 为中心;Telegram、Slack、飞书、微信等——共享同一智能体会话。
|
||||
- **🖥️ 浏览器 WebUI(beta)** — 单终端 `--ui webui` 启动带工作区面板的 Web 应用。
|
||||
@@ -145,6 +145,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
|
||||
<details>
|
||||
<summary>📦 版本更新摘要(changelog)</summary>
|
||||
|
||||
- **[2026 年 6 月 7 日]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — 辅助模型(后台任务与工具选择)、observation 记忆生命周期、Qwen3.7-Max/Plus(DashScope)、UI 后端选择,以及 OpenRouter 多轮推理修复。
|
||||
- **[2026 年 6 月 3 日]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — 多模态处理(图片 + PDF/文档 flatten/hoisting、纯文本模型回退)、runtime-context 中间件、memory 中间件迁移至 profile 文件 + stream 时间线叙述、textual 中文输入修复。
|
||||
- **[2026 年 6 月 2 日]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — 浏览器 WebUI 模式(beta)、`EvoSci deploy` 独立 LangGraph 服务器、默认模型 → claude-sonnet-4-6、MiniMax M3,以及 sandbox 超时与 async-notifier 渠道路由修复。
|
||||
- **[2026 年 5 月 19 日]** **[v0.1.1](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.1)** — deepagents 0.6.2 DeltaChannel 升级、tier-aware skill mounts、状态/耗时栏、QQ 内联按钮。
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "EvoScientist"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = "EvoScientist: Towards Self-Evolving AI Scientists for End-to-End Scientific Discovery"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
]
|
||||
dependencies = [
|
||||
"deepagents[quickjs]~=0.6.7",
|
||||
"deepagents[quickjs]~=0.6.8",
|
||||
"langchain>=1.3",
|
||||
"langchain-anthropic>=1.4",
|
||||
"langchain-openai>=1.2",
|
||||
|
||||
@@ -411,6 +411,8 @@ def test_auto_approve_still_includes_ask_user_middleware(
|
||||
cfg.enable_ask_user = True
|
||||
cfg.auto_approve = True
|
||||
cfg.auto_mode = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
|
||||
|
||||
@@ -430,6 +432,8 @@ def test_auto_mode_disables_ask_user_middleware(
|
||||
cfg.enable_ask_user = True
|
||||
cfg.auto_approve = True
|
||||
cfg.auto_mode = True
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
|
||||
|
||||
@@ -458,6 +462,8 @@ def test_for_async_subagent_omits_ask_user_middleware(
|
||||
cfg.enable_ask_user = True
|
||||
cfg.auto_approve = False
|
||||
cfg.auto_mode = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
|
||||
|
||||
|
||||
@@ -125,6 +125,8 @@ def test_inject_subagent_omits_memory_middleware_when_memory_disabled(
|
||||
cfg.memory_observations_enabled = False
|
||||
cfg.memory_observation_writer = MemoryObservationWriter.ALL
|
||||
cfg.memory_workers_enabled = True
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _inject_subagent_middleware
|
||||
@@ -153,6 +155,8 @@ def test_inject_subagent_worker_only_observation_writer_keeps_live_tool_off(
|
||||
cfg.memory_observations_enabled = True
|
||||
cfg.memory_observation_writer = MemoryObservationWriter.WORKER
|
||||
cfg.memory_workers_enabled = True
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _inject_subagent_middleware
|
||||
@@ -191,6 +195,8 @@ def test_all_observation_writer_skips_turn_worker_without_profile_memory(
|
||||
cfg.memory_observations_enabled = True
|
||||
cfg.memory_observation_writer = MemoryObservationWriter.ALL
|
||||
cfg.memory_workers_enabled = True
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
mock_chat.return_value = MagicMock(profile={"max_input_tokens": 200_000})
|
||||
|
||||
@@ -259,6 +265,8 @@ def test_async_subagent_mode_filters_ask_user(
|
||||
cfg.memory_observations_enabled = True
|
||||
cfg.memory_observation_writer = MemoryObservationWriter.ALL
|
||||
cfg.memory_workers_enabled = True
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
mock_chat.return_value = MagicMock(profile={"max_input_tokens": 200_000})
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Tests for the auxiliary-model resolver and its middleware scoping.
|
||||
|
||||
Covers ``EvoScientist.EvoScientist._ensure_auxiliary_chat_model`` (fallback to
|
||||
the main model when unset) and the wiring in ``_get_default_middleware`` that
|
||||
routes the main agent's tool selector to the auxiliary model while keeping
|
||||
context editing — and async sub-agents — on the main model.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import EvoScientist.EvoScientist as E
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_model_caches(monkeypatch):
|
||||
"""Isolate the module-level model caches per test."""
|
||||
monkeypatch.setattr(E, "_chat_model", None, raising=False)
|
||||
monkeypatch.setattr(E, "_chat_model_key", None, raising=False)
|
||||
monkeypatch.setattr(E, "_auxiliary_chat_model", None, raising=False)
|
||||
monkeypatch.setattr(E, "_auxiliary_chat_model_key", None, raising=False)
|
||||
|
||||
|
||||
def _cfg(**over):
|
||||
base = {
|
||||
"model": "main-m",
|
||||
"provider": "main-p",
|
||||
"auxiliary_model": "",
|
||||
"auxiliary_provider": "",
|
||||
}
|
||||
base.update(over)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
class TestAuxiliaryResolver:
|
||||
def test_empty_returns_main_instance(self, monkeypatch):
|
||||
main = object()
|
||||
monkeypatch.setattr(E, "_ensure_config", lambda config=None: _cfg())
|
||||
monkeypatch.setattr(E, "_ensure_chat_model", lambda: main)
|
||||
assert E._ensure_auxiliary_chat_model() is main
|
||||
|
||||
def test_aux_equal_to_main_reuses_main_instance(self, monkeypatch):
|
||||
main = object()
|
||||
monkeypatch.setattr(
|
||||
E,
|
||||
"_ensure_config",
|
||||
lambda config=None: _cfg(
|
||||
auxiliary_model="main-m", auxiliary_provider="main-p"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(E, "_ensure_chat_model", lambda: main)
|
||||
assert E._ensure_auxiliary_chat_model() is main
|
||||
|
||||
def test_set_builds_auxiliary(self, monkeypatch):
|
||||
fake = object()
|
||||
get_chat_model = MagicMock(return_value=fake)
|
||||
monkeypatch.setattr(
|
||||
E,
|
||||
"_ensure_config",
|
||||
lambda config=None: _cfg(
|
||||
auxiliary_model="aux-m", auxiliary_provider="aux-p"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr("EvoScientist.llm.get_chat_model", get_chat_model)
|
||||
assert E._ensure_auxiliary_chat_model() is fake
|
||||
get_chat_model.assert_called_once_with(model="aux-m", provider="aux-p")
|
||||
|
||||
def test_empty_provider_falls_back_to_main_provider(self, monkeypatch):
|
||||
get_chat_model = MagicMock(return_value=object())
|
||||
monkeypatch.setattr(
|
||||
E,
|
||||
"_ensure_config",
|
||||
lambda config=None: _cfg(auxiliary_model="aux-m", auxiliary_provider=""),
|
||||
)
|
||||
monkeypatch.setattr("EvoScientist.llm.get_chat_model", get_chat_model)
|
||||
E._ensure_auxiliary_chat_model()
|
||||
get_chat_model.assert_called_once_with(model="aux-m", provider="main-p")
|
||||
|
||||
def test_set_chat_model_resets_aux_cache(self, monkeypatch):
|
||||
monkeypatch.setattr(E, "_auxiliary_chat_model", object(), raising=False)
|
||||
monkeypatch.setattr(E, "_auxiliary_chat_model_key", ("x", "y"), raising=False)
|
||||
monkeypatch.setattr(
|
||||
"EvoScientist.llm.get_chat_model", MagicMock(return_value=object())
|
||||
)
|
||||
E.set_chat_model("new-m", "new-p")
|
||||
assert E._auxiliary_chat_model is None
|
||||
assert E._auxiliary_chat_model_key is None
|
||||
|
||||
|
||||
def _mock_cfg():
|
||||
cfg = MagicMock()
|
||||
cfg.enable_ask_user = False
|
||||
cfg.auto_mode = False
|
||||
cfg.auto_approve = False
|
||||
cfg.model_fallbacks = None
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
return cfg
|
||||
|
||||
|
||||
class TestAuxiliaryMiddlewareScope:
|
||||
"""``_get_default_middleware`` routes only the right components to aux."""
|
||||
|
||||
def _capture(self):
|
||||
cap: dict[str, object] = {}
|
||||
|
||||
def fake_tool_selector(*args, model=None, **kwargs):
|
||||
cap["tool_selector"] = model
|
||||
return [MagicMock()]
|
||||
|
||||
def fake_context_editing(model=None, *args, **kwargs):
|
||||
cap["context_editing"] = model
|
||||
return MagicMock()
|
||||
|
||||
return cap, fake_tool_selector, fake_context_editing
|
||||
|
||||
def test_main_agent_tool_selector_aux_context_editing_main(self):
|
||||
cap, fake_ts, fake_ce = self._capture()
|
||||
main_model, aux_model = object(), object()
|
||||
with (
|
||||
patch.object(E, "_ensure_config", return_value=_mock_cfg()),
|
||||
patch.object(E, "_ensure_chat_model", return_value=main_model),
|
||||
patch.object(E, "_ensure_auxiliary_chat_model", return_value=aux_model),
|
||||
patch(
|
||||
"EvoScientist.middleware.create_tool_selector_middleware",
|
||||
side_effect=fake_ts,
|
||||
),
|
||||
patch(
|
||||
"EvoScientist.middleware.create_context_editing_middleware",
|
||||
side_effect=fake_ce,
|
||||
),
|
||||
):
|
||||
E._get_default_middleware()
|
||||
|
||||
assert cap["tool_selector"] is aux_model
|
||||
assert cap["context_editing"] is main_model
|
||||
|
||||
def test_async_subagent_tool_selector_stays_main(self):
|
||||
cap, fake_ts, fake_ce = self._capture()
|
||||
main_model, aux_model = object(), object()
|
||||
with (
|
||||
patch.object(E, "_ensure_config", return_value=_mock_cfg()),
|
||||
patch.object(E, "_ensure_chat_model", return_value=main_model),
|
||||
patch.object(E, "_ensure_auxiliary_chat_model", return_value=aux_model),
|
||||
patch(
|
||||
"EvoScientist.middleware.create_tool_selector_middleware",
|
||||
side_effect=fake_ts,
|
||||
),
|
||||
patch(
|
||||
"EvoScientist.middleware.create_context_editing_middleware",
|
||||
side_effect=fake_ce,
|
||||
),
|
||||
):
|
||||
E._get_default_middleware(for_async_subagent=True)
|
||||
|
||||
assert cap["tool_selector"] is main_model
|
||||
assert cap["context_editing"] is main_model
|
||||
@@ -50,6 +50,8 @@ def temp_config_dir(tmp_path, monkeypatch):
|
||||
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
|
||||
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
|
||||
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
|
||||
"EVOSCIENTIST_AUXILIARY_MODEL",
|
||||
"EVOSCIENTIST_AUXILIARY_PROVIDER",
|
||||
]:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
return config_dir
|
||||
@@ -69,6 +71,8 @@ def clean_env(monkeypatch):
|
||||
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
|
||||
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
|
||||
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
|
||||
"EVOSCIENTIST_AUXILIARY_MODEL",
|
||||
"EVOSCIENTIST_AUXILIARY_PROVIDER",
|
||||
]:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
@@ -590,3 +594,39 @@ class TestApplyConfigToEnv:
|
||||
apply_config_to_env(config)
|
||||
|
||||
assert os.environ.get("OLLAMA_BASE_URL") == "http://existing:11434"
|
||||
|
||||
|
||||
class TestAuxiliaryModelConfig:
|
||||
"""auxiliary_model / auxiliary_provider config fields (plain str, optional)."""
|
||||
|
||||
def test_defaults_empty(self):
|
||||
cfg = EvoScientistConfig()
|
||||
assert cfg.auxiliary_model == ""
|
||||
assert cfg.auxiliary_provider == ""
|
||||
|
||||
def test_save_and_load_round_trip(self, temp_config_dir, clean_env):
|
||||
save_config(
|
||||
EvoScientistConfig(
|
||||
auxiliary_model="claude-haiku-4-5",
|
||||
auxiliary_provider="anthropic",
|
||||
)
|
||||
)
|
||||
loaded = load_config()
|
||||
assert loaded.auxiliary_model == "claude-haiku-4-5"
|
||||
assert loaded.auxiliary_provider == "anthropic"
|
||||
|
||||
def test_get_set_value(self, temp_config_dir, clean_env):
|
||||
save_config(EvoScientistConfig())
|
||||
assert set_config_value("auxiliary_model", "qwen3.6-flash") is True
|
||||
assert set_config_value("auxiliary_provider", "dashscope") is True
|
||||
assert get_config_value("auxiliary_model") == "qwen3.6-flash"
|
||||
assert get_config_value("auxiliary_provider") == "dashscope"
|
||||
|
||||
def test_env_overrides_file(self, temp_config_dir, monkeypatch):
|
||||
save_config(EvoScientistConfig(auxiliary_model="claude-haiku-4-5"))
|
||||
monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_MODEL", "gpt-5.5")
|
||||
monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_PROVIDER", "openai")
|
||||
|
||||
config = get_effective_config()
|
||||
assert config.auxiliary_model == "gpt-5.5"
|
||||
assert config.auxiliary_provider == "openai"
|
||||
|
||||
@@ -114,6 +114,8 @@ def test_default_middleware_includes_context_editing(mock_config, mock_model, mo
|
||||
cfg = MagicMock()
|
||||
cfg.enable_ask_user = False
|
||||
cfg.auto_approve = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
@@ -148,6 +150,8 @@ def test_context_editing_before_overflow_mapper(mock_config, mock_model, mock_ts
|
||||
cfg = MagicMock()
|
||||
cfg.enable_ask_user = False
|
||||
cfg.auto_approve = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
|
||||
+140
-2
@@ -402,13 +402,15 @@ class TestThirdPartyRouting:
|
||||
"""OpenRouter should use native 'openrouter' provider via init_chat_model."""
|
||||
mock_init.return_value = "mock_model"
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key-456")
|
||||
# Assert the DEFAULT effort, so isolate from any leaked env override.
|
||||
monkeypatch.delenv("EVOSCIENTIST_REASONING_EFFORT", raising=False)
|
||||
|
||||
get_chat_model("x-ai/grok-4.3", provider="openrouter")
|
||||
|
||||
call_kwargs = mock_init.call_args[1]
|
||||
assert call_kwargs["model_provider"] == "openrouter"
|
||||
assert call_kwargs["api_key"] == "or-key-456"
|
||||
assert call_kwargs["reasoning"] == {"effort": "high", "summary": "disabled"}
|
||||
assert call_kwargs["reasoning"] == {"effort": "high", "summary": "auto"}
|
||||
|
||||
@patch("EvoScientist.llm.models.init_chat_model")
|
||||
def test_openrouter_reasoning_user_override(self, mock_init, monkeypatch):
|
||||
@@ -435,7 +437,7 @@ class TestThirdPartyRouting:
|
||||
get_chat_model("x-ai/grok-4.3", provider="openrouter")
|
||||
|
||||
call_kwargs = mock_init.call_args[1]
|
||||
assert call_kwargs["reasoning"] == {"effort": "medium", "summary": "disabled"}
|
||||
assert call_kwargs["reasoning"] == {"effort": "medium", "summary": "auto"}
|
||||
|
||||
@patch("EvoScientist.llm.models.init_chat_model")
|
||||
def test_custom_routes_through_openai(self, mock_init, monkeypatch):
|
||||
@@ -1994,6 +1996,142 @@ class TestPatchOpenAICaptureReasoningContent:
|
||||
assert msg.additional_kwargs.get("reasoning_content") == "use the tool"
|
||||
|
||||
|
||||
class TestIsResponsesReasoningItem:
|
||||
"""_is_responses_reasoning_item flags encrypted OpenAI-Responses items."""
|
||||
|
||||
def test_rs_id_is_responses_item(self):
|
||||
from EvoScientist.llm.patches import _is_responses_reasoning_item
|
||||
|
||||
assert _is_responses_reasoning_item({"id": "rs_09363d42", "type": "x"})
|
||||
|
||||
def test_encrypted_data_is_responses_item(self):
|
||||
from EvoScientist.llm.patches import _is_responses_reasoning_item
|
||||
|
||||
assert _is_responses_reasoning_item({"data": "gAAAAAB...", "type": "x"})
|
||||
|
||||
def test_plain_text_reasoning_is_not_responses_item(self):
|
||||
from EvoScientist.llm.patches import _is_responses_reasoning_item
|
||||
|
||||
assert not _is_responses_reasoning_item(
|
||||
{"type": "reasoning.text", "text": "thinking", "index": 0}
|
||||
)
|
||||
assert not _is_responses_reasoning_item("not a dict")
|
||||
|
||||
|
||||
class TestPatchOpenrouterStripResponsesReasoning:
|
||||
"""OpenAI-Responses encrypted reasoning items (`rs_` id / encrypted data)
|
||||
are stripped from outgoing OpenRouter assistant messages, preventing the
|
||||
multi-turn "Item with id 'rs_...' not found" 400 (store=false; #37777).
|
||||
"""
|
||||
|
||||
def _apply(self):
|
||||
import langchain_openrouter.chat_models as mod
|
||||
|
||||
import EvoScientist.llm.patches as patches
|
||||
|
||||
orig = mod._convert_message_to_dict
|
||||
orig_flag = patches._openrouter_reasoning_strip_patched
|
||||
patches._openrouter_reasoning_strip_patched = False
|
||||
patches._patch_openrouter_strip_responses_reasoning()
|
||||
return patches, mod, orig, orig_flag
|
||||
|
||||
@staticmethod
|
||||
def _restore(patches, mod, orig, orig_flag):
|
||||
mod._convert_message_to_dict = orig
|
||||
patches._openrouter_reasoning_strip_patched = orig_flag
|
||||
|
||||
def test_strips_encrypted_item_drops_key_when_empty(self):
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
patches, mod, orig, orig_flag = self._apply()
|
||||
try:
|
||||
msg = AIMessage(
|
||||
content="done",
|
||||
additional_kwargs={
|
||||
"reasoning_details": [
|
||||
{
|
||||
"type": "reasoning.summary",
|
||||
"format": "openai-responses-v1",
|
||||
"id": "rs_09363d42b054",
|
||||
"data": "gAAAAAB...",
|
||||
"summary": "real reasoning text",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
result = mod._convert_message_to_dict(msg)
|
||||
# sole entry was an rs_ item → reasoning_details removed entirely.
|
||||
assert "reasoning_details" not in result
|
||||
finally:
|
||||
self._restore(patches, mod, orig, orig_flag)
|
||||
|
||||
def test_keeps_plain_text_reasoning(self):
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
patches, mod, orig, orig_flag = self._apply()
|
||||
try:
|
||||
msg = AIMessage(
|
||||
content="done",
|
||||
additional_kwargs={
|
||||
"reasoning_details": [
|
||||
{"type": "reasoning.text", "text": "thinking", "index": 0},
|
||||
{"id": "rs_abc", "data": "blob", "index": 1},
|
||||
],
|
||||
},
|
||||
)
|
||||
result = mod._convert_message_to_dict(msg)
|
||||
kept = result["reasoning_details"]
|
||||
assert len(kept) == 1
|
||||
assert kept[0]["type"] == "reasoning.text"
|
||||
finally:
|
||||
self._restore(patches, mod, orig, orig_flag)
|
||||
|
||||
def test_does_not_mutate_original_message(self):
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
patches, mod, orig, orig_flag = self._apply()
|
||||
try:
|
||||
details = [{"id": "rs_abc", "data": "blob"}]
|
||||
msg = AIMessage(
|
||||
content="x", additional_kwargs={"reasoning_details": details}
|
||||
)
|
||||
mod._convert_message_to_dict(msg)
|
||||
# stored history untouched — we filter a fresh list, not in place.
|
||||
assert details == [{"id": "rs_abc", "data": "blob"}]
|
||||
finally:
|
||||
self._restore(patches, mod, orig, orig_flag)
|
||||
|
||||
def test_patch_is_idempotent(self):
|
||||
patches, mod, orig, orig_flag = self._apply()
|
||||
try:
|
||||
wrapper = mod._convert_message_to_dict
|
||||
# Second call is guarded by the flag → must not re-wrap.
|
||||
patches._patch_openrouter_strip_responses_reasoning()
|
||||
assert mod._convert_message_to_dict is wrapper
|
||||
finally:
|
||||
self._restore(patches, mod, orig, orig_flag)
|
||||
|
||||
def test_non_dict_entry_is_kept(self):
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
patches, mod, orig, orig_flag = self._apply()
|
||||
try:
|
||||
msg = AIMessage(
|
||||
content="done",
|
||||
additional_kwargs={
|
||||
"reasoning_details": [
|
||||
"opaque", # non-dict slipped in → kept, not crashed on
|
||||
{"id": "rs_abc", "data": "blob", "index": 1},
|
||||
],
|
||||
},
|
||||
)
|
||||
result = mod._convert_message_to_dict(msg)
|
||||
assert result["reasoning_details"] == ["opaque"]
|
||||
finally:
|
||||
self._restore(patches, mod, orig, orig_flag)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test _apply_auto_config
|
||||
# =============================================================================
|
||||
|
||||
+155
-5
@@ -46,15 +46,16 @@ def _patch_all_questionary(mock_q):
|
||||
|
||||
|
||||
class TestConstants:
|
||||
def test_steps_has_twelve_items(self):
|
||||
"""Test that STEPS contains exactly 12 steps."""
|
||||
assert len(STEPS) == 12
|
||||
def test_steps_has_thirteen_items(self):
|
||||
"""Test that STEPS contains exactly 13 steps."""
|
||||
assert len(STEPS) == 13
|
||||
assert STEPS == [
|
||||
"UI",
|
||||
"LangGraph Port",
|
||||
"Provider",
|
||||
"API Key",
|
||||
"Model",
|
||||
"Auxiliary Model",
|
||||
"Tavily Key",
|
||||
"Workspace",
|
||||
"Thinking",
|
||||
@@ -353,6 +354,20 @@ class TestStepProvider:
|
||||
assert result == "anthropic"
|
||||
mock_q.select.assert_called_once()
|
||||
|
||||
def test_default_value_and_label_override(self):
|
||||
"""default_value preselects a provider (re-run co-pilot default) and
|
||||
label customizes the prompt text."""
|
||||
from EvoScientist.config.onboard.steps import _step_provider
|
||||
|
||||
config = EvoScientistConfig(provider="anthropic")
|
||||
with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
|
||||
mock_q.select.return_value.ask.return_value = "openai"
|
||||
_step_provider(config, label="co-pilot", default_value="openrouter")
|
||||
|
||||
call = mock_q.select.call_args
|
||||
assert call.kwargs["default"] == "openrouter" # override, not config.provider
|
||||
assert "co-pilot" in call.args[0]
|
||||
|
||||
def test_raises_keyboard_interrupt_on_cancel(self):
|
||||
"""Test that _step_provider raises KeyboardInterrupt on cancel."""
|
||||
from EvoScientist.config.onboard.steps import _step_provider
|
||||
@@ -378,6 +393,38 @@ class TestStepModel:
|
||||
|
||||
assert result == "claude-sonnet-4-6"
|
||||
|
||||
def test_main_model_not_in_provider_list_defaults_to_first(self):
|
||||
"""Reset/main flow: a config.model that isn't in the chosen provider's
|
||||
list (e.g. provider switched to google-genai) defaults to that
|
||||
provider's first model, NOT the custom 'Type a model name...' entry."""
|
||||
from EvoScientist.config.onboard.steps import _step_model
|
||||
from EvoScientist.llm.models import get_models_for_provider
|
||||
|
||||
config = EvoScientistConfig(model="claude-sonnet-4-6")
|
||||
entries = get_models_for_provider("google-genai")
|
||||
with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
|
||||
mock_q.select.return_value.ask.return_value = entries[0][0]
|
||||
_step_model(config, "google-genai")
|
||||
|
||||
default = mock_q.select.call_args.kwargs["default"]
|
||||
assert default == entries[0][0]
|
||||
assert default != "__custom__"
|
||||
|
||||
def test_custom_default_value_preselects_and_prefills(self):
|
||||
"""Co-pilot re-run: a saved custom (non-registry) model preselects and
|
||||
prefills the 'Type a model name...' entry."""
|
||||
from EvoScientist.config.onboard.steps import _step_model
|
||||
|
||||
config = EvoScientistConfig()
|
||||
with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
|
||||
mock_q.select.return_value.ask.return_value = "__custom__"
|
||||
mock_q.text.return_value.ask.return_value = "my-private/model"
|
||||
result = _step_model(config, "openrouter", default_value="my-private/model")
|
||||
|
||||
assert mock_q.select.call_args.kwargs["default"] == "__custom__"
|
||||
assert mock_q.text.call_args.kwargs["default"] == "my-private/model"
|
||||
assert result == "my-private/model"
|
||||
|
||||
def test_raises_keyboard_interrupt_on_cancel(self):
|
||||
"""Test that _step_model raises KeyboardInterrupt on cancel."""
|
||||
from EvoScientist.config.onboard.steps import _step_model
|
||||
@@ -1143,6 +1190,7 @@ class TestRunOnboard:
|
||||
"anthropic", # Provider
|
||||
"api_key", # Anthropic auth mode (API key, not OAuth)
|
||||
"claude-sonnet-4-6", # Model
|
||||
"skip", # Auxiliary: Skip (single driver)
|
||||
"daemon", # Workspace mode
|
||||
True, # Show thinking
|
||||
]
|
||||
@@ -1173,6 +1221,101 @@ class TestRunOnboard:
|
||||
assert final_config.ui_backend == "tui"
|
||||
assert final_config.default_mode == "daemon"
|
||||
|
||||
def test_auxiliary_model_enabled_collects_provider_and_key(self):
|
||||
"""Enabling the auxiliary step stores its provider, model, and the
|
||||
chosen provider's API key (a different company than the main agent)."""
|
||||
from EvoScientist.config.onboard.wizard import run_onboard
|
||||
|
||||
mock_q = MagicMock()
|
||||
with (
|
||||
_patch_all_questionary(mock_q),
|
||||
patch("EvoScientist.config.onboard.wizard.load_config") as mock_load,
|
||||
patch("EvoScientist.config.onboard.wizard.save_config") as mock_save,
|
||||
patch("EvoScientist.config.onboard.wizard.console"),
|
||||
patch("EvoScientist.config.onboard.steps.console"),
|
||||
patch("EvoScientist.config.onboard.channels.console"),
|
||||
patch("EvoScientist.config.onboard.helpers.console"),
|
||||
patch("EvoScientist.config.onboard.wizard._step_tinytex"),
|
||||
):
|
||||
mock_load.return_value = EvoScientistConfig()
|
||||
|
||||
mock_q.select.return_value.ask.side_effect = [
|
||||
"tui", # UI backend
|
||||
"anthropic", # Provider
|
||||
"api_key", # Anthropic auth mode
|
||||
"claude-sonnet-4-6", # Model
|
||||
"assemble", # Auxiliary: Assemble
|
||||
"openai", # Auxiliary provider (a different company)
|
||||
"gpt-5.5", # Auxiliary model
|
||||
"daemon", # Workspace mode
|
||||
True, # Show thinking
|
||||
]
|
||||
mock_q.password.return_value.ask.side_effect = [
|
||||
"", # Main provider API key (keep current)
|
||||
"sk-aux-openai", # Auxiliary provider API key
|
||||
"", # Tavily key (keep current)
|
||||
]
|
||||
mock_q.confirm.return_value.ask.side_effect = [
|
||||
True, # Save config
|
||||
]
|
||||
mock_q.text.return_value.ask.side_effect = [
|
||||
"", # Workspace directory
|
||||
]
|
||||
mock_q.checkbox.return_value.ask.return_value = [] # Skills: skip
|
||||
|
||||
result = run_onboard(skip_validation=True)
|
||||
|
||||
assert result is True
|
||||
final_config = mock_save.call_args_list[-1].args[0]
|
||||
assert final_config.auxiliary_provider == "openai"
|
||||
assert final_config.auxiliary_model == "gpt-5.5"
|
||||
# The auxiliary provider's key is stored in its per-provider field.
|
||||
assert final_config.openai_api_key == "sk-aux-openai"
|
||||
# Main agent is untouched.
|
||||
assert final_config.provider == "anthropic"
|
||||
assert final_config.model == "claude-sonnet-4-6"
|
||||
|
||||
def test_auxiliary_custom_provider_collects_base_url(self):
|
||||
"""Regression for the custom-provider fix: a custom auxiliary provider
|
||||
collects its base URL (provider -> base URL -> key -> model order)."""
|
||||
from EvoScientist.config.onboard.wizard import run_onboard
|
||||
|
||||
mock_q = MagicMock()
|
||||
with (
|
||||
_patch_all_questionary(mock_q),
|
||||
patch("EvoScientist.config.onboard.wizard.load_config") as mock_load,
|
||||
patch("EvoScientist.config.onboard.wizard.save_config") as mock_save,
|
||||
patch("EvoScientist.config.onboard.wizard.console"),
|
||||
patch("EvoScientist.config.onboard.steps.console"),
|
||||
patch("EvoScientist.config.onboard.channels.console"),
|
||||
patch("EvoScientist.config.onboard.helpers.console"),
|
||||
):
|
||||
mock_load.return_value = EvoScientistConfig()
|
||||
mock_q.select.return_value.ask.side_effect = [
|
||||
"assemble", # Auxiliary: Assemble
|
||||
"custom-openai", # Auxiliary provider
|
||||
"gpt-5.5", # Auxiliary model (from the custom-openai registry)
|
||||
]
|
||||
mock_q.text.return_value.ask.side_effect = [
|
||||
"https://my-endpoint/v1", # Auxiliary base URL (custom provider)
|
||||
]
|
||||
mock_q.password.return_value.ask.side_effect = [
|
||||
"sk-aux-custom", # Auxiliary provider API key
|
||||
]
|
||||
mock_q.confirm.return_value.ask.side_effect = [True] # Save
|
||||
|
||||
result = run_onboard(
|
||||
skip_validation=True, only_sections={"auxiliary_model"}
|
||||
)
|
||||
|
||||
assert result is True
|
||||
final_config = mock_save.call_args_list[-1].args[0]
|
||||
assert final_config.auxiliary_provider == "custom-openai"
|
||||
assert final_config.auxiliary_model == "gpt-5.5"
|
||||
# Base URL must be collected for the custom auxiliary provider.
|
||||
assert final_config.custom_openai_base_url == "https://my-endpoint/v1"
|
||||
assert final_config.custom_openai_api_key == "sk-aux-custom"
|
||||
|
||||
def test_returns_false_on_cancel(self):
|
||||
"""Test that run_onboard returns False when cancelled."""
|
||||
from EvoScientist.config.onboard.wizard import run_onboard
|
||||
@@ -1218,6 +1361,7 @@ class TestRunOnboard:
|
||||
"anthropic", # Provider
|
||||
"api_key", # Anthropic auth mode
|
||||
"claude-sonnet-4-6", # Model
|
||||
"skip", # Auxiliary: Skip (single driver)
|
||||
"daemon", # Workspace mode
|
||||
True, # Show thinking
|
||||
]
|
||||
@@ -1288,11 +1432,14 @@ class TestRunOnboard:
|
||||
"anthropic",
|
||||
"api_key",
|
||||
"claude-sonnet-4-6",
|
||||
"skip", # Auxiliary: Skip (single driver)
|
||||
"daemon",
|
||||
True,
|
||||
]
|
||||
mock_q.password.return_value.ask.side_effect = ["", ""]
|
||||
mock_q.confirm.return_value.ask.side_effect = [False] # Save? = No
|
||||
mock_q.confirm.return_value.ask.side_effect = [
|
||||
False, # Save? = No
|
||||
]
|
||||
mock_q.text.return_value.ask.side_effect = [""]
|
||||
mock_q.checkbox.return_value.ask.return_value = []
|
||||
|
||||
@@ -1335,11 +1482,14 @@ class TestRunOnboard:
|
||||
"anthropic",
|
||||
"api_key",
|
||||
"claude-sonnet-4-6",
|
||||
"skip", # Auxiliary: Skip (single driver)
|
||||
"daemon",
|
||||
True,
|
||||
]
|
||||
mock_q.password.return_value.ask.side_effect = ["", ""]
|
||||
mock_q.confirm.return_value.ask.side_effect = [False] # Save? = No
|
||||
mock_q.confirm.return_value.ask.side_effect = [
|
||||
False, # Save? = No
|
||||
]
|
||||
mock_q.text.return_value.ask.side_effect = [""]
|
||||
mock_q.checkbox.return_value.ask.return_value = []
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ def _mock_config():
|
||||
cfg.auto_mode = False
|
||||
cfg.auto_approve = False
|
||||
cfg.model_fallbacks = None
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
cfg.code_interpreter_timeout = 60
|
||||
cfg.code_interpreter_max_result_chars = 6000
|
||||
return cfg
|
||||
|
||||
@@ -161,6 +161,8 @@ def test_default_middleware_includes_tool_selector(mock_config, mock_model, mock
|
||||
cfg = MagicMock()
|
||||
cfg.enable_ask_user = False
|
||||
cfg.auto_approve = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
@@ -196,6 +198,8 @@ def test_tool_selector_ordering(mock_config, mock_model, mock_ts):
|
||||
cfg = MagicMock()
|
||||
cfg.enable_ask_user = False
|
||||
cfg.auto_approve = False
|
||||
cfg.auxiliary_model = ""
|
||||
cfg.auxiliary_provider = ""
|
||||
mock_config.return_value = cfg
|
||||
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
|
||||
@@ -810,7 +810,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "deepagents"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain" },
|
||||
@@ -820,9 +820,9 @@ dependencies = [
|
||||
{ name = "langsmith" },
|
||||
{ name = "wcmatch" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/bb/bb837a2c51631fe9d7eedf6aca7629ddca6336831801e75efcd2f5fa9c27/deepagents-0.6.7.tar.gz", hash = "sha256:af7b5857b28e29a847a4ced4cc7aaa809d33d42107696b1ca5d978d17e96b831", size = 194236, upload-time = "2026-05-30T04:42:14.591Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/77/e3b7efd9bff9cd101c085a5a3bf74180c13ab6c41a96f725cd1cb1bf53e8/deepagents-0.6.8.tar.gz", hash = "sha256:70cdd4da920cc420a8a0f729792ec559688bbbff39f7ab1508110cce9f901c06", size = 196927, upload-time = "2026-06-03T17:08:36.724Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/25/3a7f23d04778ccb95542f1b1ed39826290916221cc8203d0fb8b26c3edc1/deepagents-0.6.7-py3-none-any.whl", hash = "sha256:3518c1e5f4b9f6588ba39912b668b42b5c864b99a638e94c166d1c7176c7388e", size = 218740, upload-time = "2026-05-30T04:42:13.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/19/1b7b76e958ac7f4e40886edc70f67aff4d7188770ab68105c9c48cbeb769/deepagents-0.6.8-py3-none-any.whl", hash = "sha256:087bdc1458202a3436854cf180f7ec059d07d2114a6c232819e9ad6533a5174a", size = 221469, upload-time = "2026-06-03T17:08:35.133Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -997,7 +997,7 @@ requires-dist = [
|
||||
{ name = "certifi", marker = "extra == 'wechat'", specifier = ">=2024.0" },
|
||||
{ name = "cryptography", marker = "extra == 'all-channels'", specifier = ">=41.0" },
|
||||
{ name = "cryptography", marker = "extra == 'qq'", specifier = ">=41.0" },
|
||||
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.7" },
|
||||
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.8" },
|
||||
{ name = "discord-py", marker = "extra == 'all-channels'", specifier = ">=2.3" },
|
||||
{ name = "discord-py", marker = "extra == 'discord'", specifier = ">=2.3" },
|
||||
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
|
||||
@@ -1960,16 +1960,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/d0/c7f9d3d26c0e3f8bb146c6d707ee0fc1d30d8da65a59626e8a580085e929/langchain-1.3.2.tar.gz", hash = "sha256:ffd5f204a46b5fa1a38bf89ba3b45ca0902c02d18fa7d2a2eaeaeb1f5bf19d0a", size = 600598, upload-time = "2026-05-26T18:17:57.715Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/3f/034eb6cbef90bfccc89b7f8ed0c1d853dc9cb0bea17c7a269534c647ba3a/langchain-1.3.4.tar.gz", hash = "sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a", size = 606617, upload-time = "2026-06-02T20:04:49.411Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/82/a54edcd1c48163de5642eb10fa2cb58b13a8889c659964f63f0306b58b1e/langchain-1.3.2-py3-none-any.whl", hash = "sha256:900f6b3f4ee08b9ba3cdbe667dbf42525bd6f66a4a07a7f1db26262673e41ed6", size = 121225, upload-time = "2026-05-26T18:17:56.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/9ffe99c7dc4891a0215ec59c423bea320f943c08a231bc5bae392a438a83/langchain-1.3.4-py3-none-any.whl", hash = "sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672", size = 125286, upload-time = "2026-06-02T20:04:48.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2092,14 +2092,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-protocol"
|
||||
version = "0.0.14"
|
||||
version = "0.0.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/bf/efb5e2ed832e4d6d45590e25a9e5191986b291b543bc6a807b48bee070b0/langchain_protocol-0.0.14.tar.gz", hash = "sha256:bc1e8553122e6ede310280462d5813023a172ff2785ccbbdec54d43f3a15e5f2", size = 5862, upload-time = "2026-04-29T16:40:18.657Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff", size = 5969, upload-time = "2026-05-28T23:05:11.121Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e9/06c47ecb2aff08f83dfa30058da3bf86be64862c19569043ed5331bbeecd/langchain_protocol-0.0.14-py3-none-any.whl", hash = "sha256:ffc35089779bd8ca217015180cef5e660fc3b074efdaa0f2e95df73583f1a047", size = 6984, upload-time = "2026-04-29T16:40:17.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", size = 7037, upload-time = "2026-05-28T23:05:10.163Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2120,7 +2120,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.2.2"
|
||||
version = "1.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2130,9 +2130,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xxhash" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/43/dac5a2621c1e57f8eb7f0703f6f6fe34a5caf62f8f0fb4d2bb395bb454ea/langgraph-1.2.4.tar.gz", hash = "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", size = 720374, upload-time = "2026-06-02T17:07:37.347Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/9e/31ca236104966d7bb14ea9e93cfd73350aea8c41008ddf057b65794ed10d/langgraph-1.2.4-py3-none-any.whl", hash = "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067", size = 245402, upload-time = "2026-06-02T17:07:35.977Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2258,15 +2258,18 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.3.13"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-protocol" },
|
||||
{ name = "orjson" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4671,61 +4674,44 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "16.0"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user