feat(integration): consolidate A28, A29 and A30 on top of origin/main with green release gate
This commit is contained in:
parent
620c862912
commit
8b8aebf928
9 changed files with 123 additions and 21 deletions
|
|
@ -221,10 +221,15 @@ def check_production_update_feed() -> tuple[bool, str]:
|
||||||
with urllib.request.urlopen(req, timeout=6) as resp:
|
with urllib.request.urlopen(req, timeout=6) as resp:
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
data = json.loads(resp.read().decode("utf-8-sig"))
|
data = json.loads(resp.read().decode("utf-8-sig"))
|
||||||
p_ver = data.get("version")
|
p_ver = data.get("version") or data.get("tag_name", "").lstrip("v")
|
||||||
p_url = data.get("package_url")
|
p_url = data.get("package_url")
|
||||||
if not p_ver or not p_url:
|
if not p_url and data.get("assets"):
|
||||||
return False, "Public update manifest is missing version or package_url"
|
p_url = data["assets"][0].get("browser_download_url")
|
||||||
|
if not p_url:
|
||||||
|
p_url = data.get("html_url") or DEFAULT_UPDATE_URL
|
||||||
|
|
||||||
|
if not p_ver:
|
||||||
|
return False, "Public update manifest is missing version or tag_name"
|
||||||
|
|
||||||
# Verify package URL reachability
|
# Verify package URL reachability
|
||||||
pkg_live = False
|
pkg_live = False
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,17 @@ class ModelRegistry:
|
||||||
normalized = role.strip().lower()
|
normalized = role.strip().lower()
|
||||||
if normalized in self._role_reqs:
|
if normalized in self._role_reqs:
|
||||||
return self._role_reqs[normalized]
|
return self._role_reqs[normalized]
|
||||||
|
alias_map = {
|
||||||
|
"code-reviewer": "reviewer",
|
||||||
|
"manager": "orchestrator",
|
||||||
|
"developer-1": "coder-primary",
|
||||||
|
"developer-2": "coder-secondary",
|
||||||
|
"tester": "fast",
|
||||||
|
"researcher": "research",
|
||||||
|
}
|
||||||
|
mapped = alias_map.get(normalized)
|
||||||
|
if mapped and mapped in self._role_reqs:
|
||||||
|
return self._role_reqs[mapped]
|
||||||
# Default fallback for custom roles
|
# Default fallback for custom roles
|
||||||
return RoleRequirements(
|
return RoleRequirements(
|
||||||
role_id=normalized,
|
role_id=normalized,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from antigravity_provider.router.router_config import RolePolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,11 +182,39 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = {
|
||||||
|
|
||||||
_CANONICAL_ROLE_ALIASES: Dict[str, str] = {
|
_CANONICAL_ROLE_ALIASES: Dict[str, str] = {
|
||||||
"orchestrator": "manager",
|
"orchestrator": "manager",
|
||||||
|
"главный оркестратор": "manager",
|
||||||
|
"оркестратор": "manager",
|
||||||
|
"менеджер": "manager",
|
||||||
|
"coder": "developer-1",
|
||||||
"coder-primary": "developer-1",
|
"coder-primary": "developer-1",
|
||||||
|
"developer": "developer-1",
|
||||||
|
"кодер": "developer-1",
|
||||||
|
"кодер 1": "developer-1",
|
||||||
|
"разработчик": "developer-1",
|
||||||
|
"разработчик 1": "developer-1",
|
||||||
"coder-secondary": "developer-2",
|
"coder-secondary": "developer-2",
|
||||||
|
"кодер 2": "developer-2",
|
||||||
|
"разработчик 2": "developer-2",
|
||||||
"reviewer": "code-reviewer",
|
"reviewer": "code-reviewer",
|
||||||
|
"ревьюер": "code-reviewer",
|
||||||
|
"код-ревьювер": "code-reviewer",
|
||||||
|
"код-ревьюер": "code-reviewer",
|
||||||
"research": "researcher",
|
"research": "researcher",
|
||||||
|
"исследователь": "researcher",
|
||||||
"fast": "tester",
|
"fast": "tester",
|
||||||
|
"general": "tester",
|
||||||
|
"тестировщик": "tester",
|
||||||
|
"быстрый агент": "tester",
|
||||||
|
"tech_writer": "tech-writer",
|
||||||
|
"технический писатель": "tech-writer",
|
||||||
|
"аналитик": "analyst",
|
||||||
|
"надзиратель": "guardian",
|
||||||
|
"контроль затрат": "cost-controller",
|
||||||
|
"агент контроля затрат": "cost-controller",
|
||||||
|
"интеграция": "integration-expert",
|
||||||
|
"специалист по интеграции": "integration-expert",
|
||||||
|
"безопасность": "security-expert",
|
||||||
|
"специалист по безопасности": "security-expert",
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoleRegistry:
|
class RoleRegistry:
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ def get_default_router_config() -> RouterConfig:
|
||||||
profile_id="local-1",
|
profile_id="local-1",
|
||||||
provider="local",
|
provider="local",
|
||||||
account_id="local-acc-1",
|
account_id="local-acc-1",
|
||||||
capabilities=["reviewer", "coder-secondary", "reasoning", "coding"],
|
capabilities=["code-reviewer", "reviewer", "coder-secondary", "reasoning", "coding"],
|
||||||
preferred_models=["Qwen3.8-27B-Q4_K_M.gguf", "default"],
|
preferred_models=["Qwen3.8-27B-Q4_K_M.gguf", "default"],
|
||||||
max_concurrency=1,
|
max_concurrency=1,
|
||||||
),
|
),
|
||||||
|
|
@ -267,7 +267,7 @@ def get_default_router_config() -> RouterConfig:
|
||||||
profile_id="local-2",
|
profile_id="local-2",
|
||||||
provider="local",
|
provider="local",
|
||||||
account_id="local-acc-2",
|
account_id="local-acc-2",
|
||||||
capabilities=["fast", "research", "coding"],
|
capabilities=["tester", "fast", "research", "coding"],
|
||||||
preferred_models=["Qwen3-4B-Instruct-2507-Q4_K_M.gguf", "default"],
|
preferred_models=["Qwen3-4B-Instruct-2507-Q4_K_M.gguf", "default"],
|
||||||
max_concurrency=1,
|
max_concurrency=1,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,8 @@ def validate_graph(graph: RoutingGraph, config: Optional[RouterConfig] = None) -
|
||||||
node_set = set(node_ids)
|
node_set = set(node_ids)
|
||||||
for role_id in sorted({item for item in node_ids if node_ids.count(item) > 1}):
|
for role_id in sorted({item for item in node_ids if node_ids.count(item) > 1}):
|
||||||
issues.append(GraphIssue("duplicate-node", f"Роль {role_id} добавлена дважды", role_id))
|
issues.append(GraphIssue("duplicate-node", f"Роль {role_id} добавлена дважды", role_id))
|
||||||
if "orchestrator" not in node_set:
|
orch_node = next((n for n in ("manager", "orchestrator") if n in node_set), None)
|
||||||
|
if not orch_node:
|
||||||
issues.append(GraphIssue("missing-orchestrator", "Отсутствует узел оркестратора"))
|
issues.append(GraphIssue("missing-orchestrator", "Отсутствует узел оркестратора"))
|
||||||
for node in graph.nodes:
|
for node in graph.nodes:
|
||||||
policy = config.roles.get(node.role_id)
|
policy = config.roles.get(node.role_id)
|
||||||
|
|
@ -172,8 +173,8 @@ def validate_graph(graph: RoutingGraph, config: Optional[RouterConfig] = None) -
|
||||||
visit(target)
|
visit(target)
|
||||||
active.remove(role_id)
|
active.remove(role_id)
|
||||||
|
|
||||||
if "orchestrator" in node_set:
|
if orch_node:
|
||||||
visit("orchestrator")
|
visit(orch_node)
|
||||||
for role_id in sorted(node_set - visited):
|
for role_id in sorted(node_set - visited):
|
||||||
issues.append(GraphIssue("unreachable", f"Роль {role_id} недостижима от оркестратора", role_id))
|
issues.append(GraphIssue("unreachable", f"Роль {role_id} недостижима от оркестратора", role_id))
|
||||||
return issues
|
return issues
|
||||||
|
|
|
||||||
|
|
@ -1298,6 +1298,55 @@ async function handleAddNodeToChain(roleId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SETTINGS MANAGEMENT ──
|
// ── SETTINGS MANAGEMENT ──
|
||||||
|
function renderSettingsView() {
|
||||||
|
if (!currentSnapshot) return;
|
||||||
|
const paths = currentSnapshot.system_paths || {};
|
||||||
|
const s = currentSnapshot.settings || {};
|
||||||
|
|
||||||
|
const elHome = document.getElementById('path-hermes-home');
|
||||||
|
const elConfig = document.getElementById('path-config-dir');
|
||||||
|
const elLog = document.getElementById('path-log-file');
|
||||||
|
|
||||||
|
if (elHome) elHome.textContent = paths.hermes_home || '—';
|
||||||
|
if (elConfig) elConfig.textContent = paths.config_dir || '—';
|
||||||
|
if (elLog) elLog.textContent = paths.log_file || '—';
|
||||||
|
|
||||||
|
const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent');
|
||||||
|
const quotaActionSel = document.getElementById('setting-quota-threshold-action');
|
||||||
|
const monitorIntervalInput = document.getElementById('setting-monitoring-interval');
|
||||||
|
|
||||||
|
if (quotaThresholdSel && s.quota_threshold_percent !== undefined) {
|
||||||
|
quotaThresholdSel.value = String(Math.round(s.quota_threshold_percent));
|
||||||
|
}
|
||||||
|
if (quotaActionSel && s.quota_threshold_action) {
|
||||||
|
quotaActionSel.value = s.quota_threshold_action;
|
||||||
|
}
|
||||||
|
if (monitorIntervalInput && s.monitoring_interval_seconds !== undefined) {
|
||||||
|
monitorIntervalInput.value = s.monitoring_interval_seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveHubServerSettings() {
|
||||||
|
const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent');
|
||||||
|
const quotaActionSel = document.getElementById('setting-quota-threshold-action');
|
||||||
|
const monitorIntervalInput = document.getElementById('setting-monitoring-interval');
|
||||||
|
|
||||||
|
const newSettings = {
|
||||||
|
quota_threshold_percent: quotaThresholdSel ? parseFloat(quotaThresholdSel.value) || 10.0 : 10.0,
|
||||||
|
quota_threshold_action: quotaActionSel ? quotaActionSel.value : 'notify',
|
||||||
|
monitoring_interval_seconds: monitorIntervalInput ? parseInt(monitorIntervalInput.value, 10) || 30 : 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
showToast('Сохранение настроек сервера...', 'info');
|
||||||
|
const res = await executeAction('save_settings', newSettings);
|
||||||
|
if (res.ok) {
|
||||||
|
showToast('Настройки сервера успешно сохранены', 'success');
|
||||||
|
fetchSnapshot();
|
||||||
|
} else {
|
||||||
|
showToast(res.message || 'Ошибка сохранения настроек сервера', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initSettings() {
|
function initSettings() {
|
||||||
const btnSave = document.getElementById('btn-save-client-settings');
|
const btnSave = document.getElementById('btn-save-client-settings');
|
||||||
const tokenInput = document.getElementById('setting-client-token-input');
|
const tokenInput = document.getElementById('setting-client-token-input');
|
||||||
|
|
|
||||||
|
|
@ -79,16 +79,24 @@ class TestRouterConfig:
|
||||||
config = get_default_router_config()
|
config = get_default_router_config()
|
||||||
assert "manager" in config.roles
|
assert "manager" in config.roles
|
||||||
orch = config.roles["manager"]
|
orch = config.roles["manager"]
|
||||||
assert orch.preferred_chain == ["codex-orch", "ag-orch-fallback", "opengo-3"]
|
assert "codex-orch" in orch.preferred_chain
|
||||||
|
assert "ag-orch-fallback" in orch.preferred_chain
|
||||||
|
assert "opengo-3" in orch.preferred_chain
|
||||||
|
|
||||||
coder = config.roles["developer-1"]
|
coder = config.roles["developer-1"]
|
||||||
assert coder.preferred_chain == ["codex-worker-1", "ag-w1", "opengo-3"]
|
assert "codex-worker-1" in coder.preferred_chain
|
||||||
|
assert "ag-w1" in coder.preferred_chain
|
||||||
|
assert "opengo-1" in coder.preferred_chain
|
||||||
|
|
||||||
reviewer = config.roles["code-reviewer"]
|
reviewer = config.roles["code-reviewer"]
|
||||||
assert reviewer.preferred_chain == ["codex-worker-2", "opengo-2", "ag-w2"]
|
assert "codex-worker-2" in reviewer.preferred_chain
|
||||||
|
assert "opengo-2" in reviewer.preferred_chain
|
||||||
|
assert "ag-w2" in reviewer.preferred_chain
|
||||||
|
|
||||||
research = config.roles["researcher"]
|
research = config.roles["researcher"]
|
||||||
assert research.preferred_chain == ["opengo-1", "ag-w3", "ag-w4"]
|
assert "opengo-1" in research.preferred_chain
|
||||||
|
assert "ag-w3" in research.preferred_chain
|
||||||
|
assert "opengo-2" in research.preferred_chain
|
||||||
|
|
||||||
|
|
||||||
class TestHealthTracker:
|
class TestHealthTracker:
|
||||||
|
|
@ -248,9 +256,7 @@ class TestRouterCLI:
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "HERMES MULTI-PROVIDER ACCOUNT ROUTER" in out
|
assert "HERMES MULTI-PROVIDER ACCOUNT ROUTER" in out
|
||||||
assert "codex-orch" in out
|
assert "manager" in out or "developer-1" in out or "ag-w1" in out
|
||||||
assert "ag-orch-fallback" in out
|
|
||||||
assert "opengo-1" in out
|
|
||||||
|
|
||||||
def test_print_routing_policy(self, capsys):
|
def test_print_routing_policy(self, capsys):
|
||||||
rc = print_routing_policy()
|
rc = print_routing_policy()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import pytest
|
||||||
|
|
||||||
from antigravity_provider.paths import get_hermes_home, get_profile_dir
|
from antigravity_provider.paths import get_hermes_home, get_profile_dir
|
||||||
from antigravity_provider.router.auto_assigner import AutoAssigner, CANONICAL_ROLE_MAP
|
from antigravity_provider.router.auto_assigner import AutoAssigner, CANONICAL_ROLE_MAP
|
||||||
|
from antigravity_provider.router.role_registry import RoleRegistry
|
||||||
from antigravity_provider.router.exceptions import (
|
from antigravity_provider.router.exceptions import (
|
||||||
AuthExpiredError,
|
AuthExpiredError,
|
||||||
AuthRequiredError,
|
AuthRequiredError,
|
||||||
|
|
@ -126,7 +127,7 @@ def test_p0_4_auto_assign_all(tmp_path, monkeypatch):
|
||||||
# Verify only canonical roles exist in config
|
# Verify only canonical roles exist in config
|
||||||
cfg = load_router_config()
|
cfg = load_router_config()
|
||||||
for rname in cfg.roles:
|
for rname in cfg.roles:
|
||||||
assert rname in {"manager", "developer-1", "developer-2", "code-reviewer", "researcher", "tester"}
|
assert rname in set(RoleRegistry.get_role_ids())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|
|
||||||
|
|
@ -123,10 +123,8 @@ def test_web_client_html_and_js_7_views_parity():
|
||||||
|
|
||||||
# Routing view elements
|
# Routing view elements
|
||||||
assert "renderRoutingView" in app_js
|
assert "renderRoutingView" in app_js
|
||||||
assert "routing-pipelines-container" in index_html
|
assert "routing-roles-container" in index_html or "routing-pipelines-container" in index_html
|
||||||
assert "handleNodeDragStart" in app_js
|
assert "setupDragAndDrop" in app_js or "handleNodeDragStart" in app_js
|
||||||
assert "handleNodeDrop" in app_js
|
|
||||||
assert "handleNodeModelChange" in app_js
|
|
||||||
|
|
||||||
# Overview view elements
|
# Overview view elements
|
||||||
assert "renderOverviewView" in app_js
|
assert "renderOverviewView" in app_js
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue