fix(review): apply P0/P2 findings from dual-agent review
- Dockerfile: split ENTRYPOINT/CMD to align with docker-compose serve - test_termbase: guard jieba import with pytest.importorskip - orchestrator: mark silent review-degradation with [DEGRADED] prefix - chat.py: accurate ExecutionMode log message - agentkit.yaml: document OTel exporter config - skill_routing: replace 12 Any with object/typed (AGENTS.md compliance) - AssistantText.vue: add aria-live/role for a11y
This commit is contained in:
parent
0962df11b5
commit
a3cecd4b50
|
|
@ -31,4 +31,7 @@ EXPOSE 8001
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8001/api/v1/health')"
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8001/api/v1/health')"
|
||||||
|
|
||||||
CMD ["uvicorn", "configs.geo_server:create_geo_app", "--factory", "--host", "0.0.0.0", "--port", "8001"]
|
# ponytail: 与 docker-compose.yaml command 对齐,纯 `docker run` 启动完整 AgentKit
|
||||||
|
# 而非 GEO 子系统。GEO 子系统应通过独立 image 或 ENTRYPOINT 参数切换。
|
||||||
|
ENTRYPOINT ["agentkit"]
|
||||||
|
CMD ["serve", "--host", "0.0.0.0", "--port", "8001"]
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,9 @@ experts: {paths: ["./configs/experts"]}
|
||||||
board: {max_rounds: 5, default_template: private_board, parallel_speech: true, history_compression_threshold: 20}
|
board: {max_rounds: 5, default_template: private_board, parallel_speech: true, history_compression_threshold: 20}
|
||||||
logging: {level: INFO, format: text}
|
logging: {level: INFO, format: text}
|
||||||
router: {classifier: heuristic, auction_enabled: false}
|
router: {classifier: heuristic, auction_enabled: false}
|
||||||
|
# OTel 可观测性 — 默认注释(OTel 为可选依赖,未安装时 telemetry/metrics.py 返回 NoOp)。
|
||||||
|
# 启用:pip install opentelemetry-sdk opentelemetry-exporter-otlp,取消注释并指向 collector。
|
||||||
|
# 未配置时所有指标(请求量/延迟/token 消耗)静默丢弃,形成监控盲区。
|
||||||
|
# telemetry:
|
||||||
|
# otlp_endpoint: http://localhost:4317 # OTLP gRPC 端点
|
||||||
|
# service_name: fischer-agentkit
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import enum
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -48,7 +47,7 @@ _SKILL_EXECUTION_MODE_MAP: dict[str, ExecutionMode] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_execution_mode(skill_config: Any) -> ExecutionMode:
|
def _resolve_execution_mode(skill_config: object) -> ExecutionMode:
|
||||||
"""Resolve ExecutionMode from skill config's execution_mode field."""
|
"""Resolve ExecutionMode from skill config's execution_mode field."""
|
||||||
mode_str = getattr(skill_config, "execution_mode", "react") or "react"
|
mode_str = getattr(skill_config, "execution_mode", "react") or "react"
|
||||||
return _SKILL_EXECUTION_MODE_MAP.get(mode_str, ExecutionMode.SKILL_REACT)
|
return _SKILL_EXECUTION_MODE_MAP.get(mode_str, ExecutionMode.SKILL_REACT)
|
||||||
|
|
@ -67,11 +66,11 @@ class SkillRoutingResult:
|
||||||
"""Result of skill routing for a user message."""
|
"""Result of skill routing for a user message."""
|
||||||
|
|
||||||
skill_name: str | None = None
|
skill_name: str | None = None
|
||||||
skill_config: Any = None
|
skill_config: object | None = None
|
||||||
skill_tools: list = field(default_factory=list)
|
skill_tools: list[object] = field(default_factory=list)
|
||||||
clean_content: str = ""
|
clean_content: str = ""
|
||||||
system_prompt: str | None = None
|
system_prompt: str | None = None
|
||||||
tools: list = field(default_factory=list)
|
tools: list[object] = field(default_factory=list)
|
||||||
model: str = "default"
|
model: str = "default"
|
||||||
agent_name: str | None = None
|
agent_name: str | None = None
|
||||||
matched: bool = False
|
matched: bool = False
|
||||||
|
|
@ -112,9 +111,9 @@ def format_preconditions_block(preconditions: list[str], header_level: int = 2)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def collect_prompt_parts(config: Any, with_headers: bool = False) -> list[str]:
|
def collect_prompt_parts(config: object, with_headers: bool = False) -> list[str]:
|
||||||
"""从 skill config 的 prompt 字典中收集各部分文本。"""
|
"""从 skill config 的 prompt 字典中收集各部分文本。"""
|
||||||
prompt = config.prompt or {}
|
prompt = getattr(config, "prompt", None) or {}
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
for key in _PROMPT_KEYS:
|
for key in _PROMPT_KEYS:
|
||||||
val = prompt.get(key)
|
val = prompt.get(key)
|
||||||
|
|
@ -167,12 +166,12 @@ def build_skill_system_prompt(skill_config) -> str | None:
|
||||||
|
|
||||||
async def resolve_skill_routing(
|
async def resolve_skill_routing(
|
||||||
content: str,
|
content: str,
|
||||||
skill_registry: Any,
|
skill_registry: object,
|
||||||
default_tools: list,
|
default_tools: list[object],
|
||||||
default_system_prompt: str | None,
|
default_system_prompt: str | None,
|
||||||
default_model: str = "default",
|
default_model: str = "default",
|
||||||
default_agent_name: str = "default",
|
default_agent_name: str = "default",
|
||||||
agent_tool_registry: Any = None,
|
agent_tool_registry: object | None = None,
|
||||||
session_id: str = "",
|
session_id: str = "",
|
||||||
) -> SkillRoutingResult:
|
) -> SkillRoutingResult:
|
||||||
"""Resolve skill routing for a user message.
|
"""Resolve skill routing for a user message.
|
||||||
|
|
@ -267,7 +266,7 @@ async def resolve_skill_routing(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _build_tools_description(tools: list) -> str:
|
def _build_tools_description(tools: list[object]) -> str:
|
||||||
"""Build a text description of tools for the system prompt."""
|
"""Build a text description of tools for the system prompt."""
|
||||||
lines = []
|
lines = []
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
|
|
|
||||||
|
|
@ -994,7 +994,9 @@ class TeamOrchestrator:
|
||||||
gateway = self._get_llm_gateway(lead)
|
gateway = self._get_llm_gateway(lead)
|
||||||
if not gateway:
|
if not gateway:
|
||||||
logger.warning("No LLM gateway available, skipping review")
|
logger.warning("No LLM gateway available, skipping review")
|
||||||
return True, "LLM 验收不可用,自动通过"
|
# 优雅降级:不阻塞流程,但 [DEGRADED] 前缀让 review_result 事件
|
||||||
|
# 和日志聚合可识别降级路径,便于运维监控验收失效频率。
|
||||||
|
return True, "[DEGRADED] LLM 验收不可用,自动通过"
|
||||||
|
|
||||||
content = result.get("content", str(result))
|
content = result.get("content", str(result))
|
||||||
# P1: prompt injection 防护 — 用 XML 标签包裹专家输出,指示 LLM 忽略其中指令
|
# P1: prompt injection 防护 — 用 XML 标签包裹专家输出,指示 LLM 忽略其中指令
|
||||||
|
|
@ -1039,8 +1041,8 @@ class TeamOrchestrator:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Review LLM call failed: {e}")
|
logger.warning(f"Review LLM call failed: {e}")
|
||||||
|
|
||||||
# 降级:验收通过(标注降级原因,便于追踪)
|
# 降级:不阻塞流程,但 [DEGRADED] 前缀让 review_result 事件可识别降级路径
|
||||||
return True, "LLM 验收降级,自动通过"
|
return True, "[DEGRADED] LLM 验收降级,自动通过"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_risk_flags(content: str) -> list[str]:
|
def _parse_risk_flags(content: str) -> list[str]:
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,18 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="message.content" ref="markdownRef" class="assistant-text__markdown" v-html="renderedContent"></div>
|
<div
|
||||||
|
v-if="message.content"
|
||||||
|
ref="markdownRef"
|
||||||
|
class="assistant-text__markdown"
|
||||||
|
role="region"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-label="助手回复内容"
|
||||||
|
v-html="renderedContent"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div v-else-if="isLoading" class="assistant-text__loading">
|
<div v-else-if="isLoading" class="assistant-text__loading" role="status" aria-label="助手正在思考">
|
||||||
<a-spin size="small" />
|
<a-spin size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1234,8 +1234,8 @@ async def _handle_chat_message(
|
||||||
ExecutionMode.PLAN_EXEC,
|
ExecutionMode.PLAN_EXEC,
|
||||||
):
|
):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Execution mode {routing.execution_mode.value} not yet supported "
|
f"Execution mode {routing.execution_mode.value} not implemented "
|
||||||
f"in chat WebSocket, falling back to REACT"
|
f"in chat WebSocket path, falling back to REACT"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute Agent with streaming
|
# Execute Agent with streaming
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import jieba
|
import pytest
|
||||||
|
|
||||||
|
# jieba 是可选依赖(pyproject.toml 主依赖),但测试环境可能未安装。
|
||||||
|
# importorskip 确保收集阶段不中断,符合 project_rules.md 的 pre-commit 门禁。
|
||||||
|
pytest.importorskip("jieba")
|
||||||
|
import jieba # noqa: E402 — 必须在 importorskip 之后
|
||||||
|
|
||||||
from agentkit.rag_platform.termbase import TermEntry, Termbase
|
from agentkit.rag_platform.termbase import TermEntry, Termbase
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue