Files
AgentLab/backend/app/engine/provider_refs.py
T
ThearchyHelios 7255ae98a5 模型接入报错收尾(第 1 轮复核,后端):交给设置的核查、裁判落到演示模型照实说;给不出修复时说卡在哪里;接入名恰好是模型名那句按这一处检查器里有哪几栏写;修复规划只重算被改的节点;助手栏那一格另给一份说法;几处措辞对齐
- 「不指定接入」交给设置(核查模型、裁判模型)时:启用的接入全是演示类,设置这一级不管写的是什么,执行时落到的都是
  演示模型,说明照实带上「(演示用,不调用真实模型)」、候选带 demo 标记;一个启用的接入都没有时不算落得稳(同别处
  「换成内置演示模型」不算)。以前 land() 在判演示之前就把交给设置的一处当成落稳返回了;
- 检查器里没有下拉框、发布前检查又给不出修复时,提示末尾补「无法改用其他接入:」加第一处落不稳的地方(Landing.why,
  比如「成员「研究员」指定的模型「小模型」所属的模型接入「旧接入」已停用」),不再只剩「核对名称」让人去设置里找一个
  找不到的名字;
- 点名的接入名恰好是某个启用接入的模型:画布、发布前检查按这一处在检查器里有哪几栏写(provider_refs._model_note)——
  模型调用一类在节点的「模型」中选择它,接入在发布前检查里改用这个接入或选择「不指定接入」(修复里有哪项说哪项);
  协作成员、全图默认只说可在发布前检查里改用;核查节点、报告自己的结论句裁判两栏都在,照设置页写这两栏的叫法。
  不再叫人去一个没有的「接入」栏留空。运行时报错不分节点类型,「填写」改为「选择它」;
- 修复候选修完以后落到演示类接入的带 demo 标记,发布弹窗据此在选项名旁加醒目的标签;
- 性能:改一个节点的配置只影响它自己取模型的那几处,修复规划(autofix._ProviderPlan)每试一个候选只重算这个节点
  (provider_refs.node_uses),不再深拷贝、重算整张图;问题提示只问给不给得出(provider_fixable,找到第一个落得稳的
  就停);接入问题的引用整张图只算一遍。60 个节点都点名同一个停用接入时,画布校验里这一部分约 0.35 秒降到约 0.002 秒,
  发布前检查约 0.72 秒降到约 0.01 秒,一次应用 30 条修复约 2.7 秒降到约 0.09 秒;
- 助手:GET /api/copilot/model 另给 panel_error(写在助手栏「助手使用的模型」那一格下面,去向写「上方」,不把人指回这个
  下拉框本身);接入名恰好是模型名时并成一句,不再两次提「助手使用的模型」;PUT /api/settings 里助手那一项的 422 用
  助手报错那一句(那里只有一个选模型的下拉框,没有「换一个接入」的地方);
- 裁判设置的格式检查写「留空表示跟随助手的模型」,同设置页空选项(engine/judge.py 一行,同核查那条的改法);「交给助手」
  后只剩接入问题时写「助手无法处理」,有修复时另起半句「也可在各条问题下方选择改用的模型接入」。

测试:test_provider_ref_wording 加演示落在设置上、demo 标记、给不出修复时卡在哪里(含一个启用接入都没有)、各处的
接入名恰好是模型名、助手栏那一格的说法、助手那一项的 422、裁判格式检查、规划开销随节点数线性增长(数试算时算了多少处
取模型的地方);其余几个文件跟上新说法。新加的核心用例都做过去掉实现、测试变红的验证。
2026-10-06 19:43:44 +08:00

376 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""工作流里点名的模型接入:不存在或已停用的,画布校验、发布前检查、发布时照实提出来。
运行时点名的接入不存在或已停用直接报错(providers/factory.named_provider),以前是悄悄换成演示模型。这类问题发布前
就该看得到:画布校验给警告(探索运行仍可发起,执行到那里报错),发布、发布前检查、自动修复的复核给错误、挡住
发布——正式运行到那里一定失败,或者少一块核查。
查的是执行时真会去取模型的那几处,取法和执行器一致(uses):
- 模型调用、Agent、报告撰写、多 Agent 协作的接入;条件分支只在「让模型分类」时、结构校验只在「让模型修复」且允许
重试时(max_retries 为 0 一次都不修)、知识检索只在「让模型重排」时才取模型。这几类节点没写自己的接入、模型时
各自取全图默认(NodeContext.cfg);
- 多 Agent 协作各成员:自己没写的接入、模型各自跟随节点(再到全图默认,nodes/multi._member);报告撰写交给模型
裁判结论句时,「结论句裁判」里的接入、模型(节点没写裁判配置时取全图默认里的那一份,同 nodes/report.report_judge);
- 双盲复算、反方审稿的接入、模型(核查模型):只读节点自己的配置,不取全图默认。节点自己的开关关着时执行器直接
跳过、不取模型,不查;设置里的总开关不看——它随时会被打开,打开时这一处就会失败;
- 全图默认的接入、模型:有节点会取到它时才查。
取到了接入的,查点名的那个接入(不存在、已停用);哪一级都没写接入、只写了模型的,按模型名找接入,只有停用的接入
提供它时同样是问题(provider.disabled,落在写模型的那一处)——运行时 resolve_provider 报的就是这一句,而多数节点的
检查器里只能选模型,这是最常见的写法。核查、裁判的接入、模型都没写时交给设置,不归这里查。
说法分两种:检查器里有这一项下拉框的几处(双盲复算、反方审稿、节点自己的结论句裁判的接入和模型,几类节点自己的模型)
说「在节点中换一个接入 / 模型」;别处(节点自己的接入、协作成员、全图默认)界面上没有下拉框,说「在发布前检查中改用
其他接入」——发布前检查给这类问题配了修复(engine/autofix.py 的 provider.*:改用一个启用的接入,或不再指定),修复
按 land 算出修完以后执行时落到哪里,落不稳的候选不给。一个候选都落不稳时修复为空,这时不指到发布前检查(那里什么
都不给),说去设置里核对、启用,再说卡在哪里(第一处落不稳的地方,比如成员只写了停用接入的模型)——不然人去设置里
找那个写错的名字,找不到,也看不出真正挡住修复的是别处。点名的名字恰好是某个启用接入的模型时,末尾补一句这是哪个
接入的模型、在这一处怎么用它(_model_note:按检查器里有哪几栏写,不叫人去一个没有的「接入」栏留空)。
节点没写模型、点名的接入(自己的或全图默认的)本身不存在或已停用时,校验那条「没有指定模型,将使用接入「X」的
默认模型」(schema.MODEL_UNSET)和这里的问题互相矛盾,由 quiet_unset_model 去掉前者。
接入表要读库,门禁本身不查库(governance.lint_for_publish),所以由接口这一层读出接入表交进来,同数据目录
SQL 检查器的做法。设置里点名接入的几项由 api/settings.py 在保存时查。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from sqlalchemy.ext.asyncio import AsyncSession
from app.engine.labels import field_label, judge_label, type_label
from app.engine.schema import CLAIMS_JUDGE, MODEL_UNSET, GraphNode, GraphSpec, NodeType, ValidationIssue
from app.providers.factory import (
NAMED_FIX, ProviderNotConfigured, fallback_provider, list_providers, model_fact, model_note, named_provider,
provider_for_model, serves,
)
#: 这类问题的编号:provider.missing(点名的接入不存在)、provider.disabled(已停用;只写了模型、提供它的接入停用了也是它)
CODES = frozenset({"provider.missing", "provider.disabled"})
#: 提示的后半句:这一处写的是接入还是模型、检查器里有没有这一项的下拉框。检查器里没有下拉框的,发布前检查给了修复
#: 才指到那里(IN_CHECK),否则不写后半句
_WHERE = {"provider": "或在节点中换一个接入", "model": "或在节点中换一个模型"}
IN_CHECK = "或在「发布前检查」中改用其他接入"
#: 检查器里没有下拉框、发布前检查也给不出修复时,提示末尾说卡在哪里(Landing.why)
NO_SWITCH = "无法改用其他接入:"
#: 一个启用的接入都没有:没点名接入的执行时换成内置演示模型,点名的调不了
NO_ENABLED = "没有启用的模型接入"
#: 修完以后执行时落到演示类接入(kind 为 mock):说明里照实写。K1 要防的就是悄悄换成演示模型——落到它是人选的,
#: 得让人知道选的是什么
DEMO_LANDING = "(演示用,不调用真实模型)"
#: 检查器里有模型下拉框的几类节点(前端 nodeDefs 的 MODEL_FIELDS;条件分支只在「让模型分类」时显示,也只在那时才查)
_MODEL_PICKER = frozenset({NodeType.LLM, NodeType.AGENT, NodeType.SUPERVISOR, NodeType.BRANCH, NodeType.REPORT})
#: 核查、裁判的接入和模型都没写时执行用设置里的哪一项(api/settings.verify_model_spec、judge.judge_model_spec)
_SETTINGS = {"verify": "设置中的核查模型", "judge": "设置中的裁判模型"}
def _cfg(spec: GraphSpec, node: GraphNode, key: str, default: Any = None) -> Any:
"""和 NodeContext.cfg 同一个规则:节点上没写(None、空串)就取全图默认。"""
value = node.config.get(key)
return spec.defaults.get(key, default) if value in (None, "") else value
def _repairs(spec: GraphSpec, node: GraphNode) -> bool:
"""结构校验会不会让模型修复:允许修复,而且至少重试一次(nodes/human.run_validate 同一个取值)。认不出的重试次数
按会修复算:执行时它照样报错,这里宁可多提一句。"""
if not _cfg(spec, node, "repair_with_llm", True):
return False
try:
return int(_cfg(spec, node, "max_retries", 2) or 0) > 0
except (TypeError, ValueError):
return True
#: 按 NodeContext.cfg 取接入的节点 → 执行时真会去取模型的条件(None 是总会取)
_TAKES_MODEL: dict[NodeType, Callable[[GraphSpec, GraphNode], bool] | None] = {
NodeType.LLM: None,
NodeType.AGENT: None,
NodeType.REPORT: None,
NodeType.SUPERVISOR: None,
NodeType.BRANCH: lambda spec, node: _cfg(spec, node, "mode", "expression") == "llm",
NodeType.VALIDATE: _repairs,
NodeType.RETRIEVE: lambda spec, node: _cfg(spec, node, "rerank", "off") == "model",
}
#: 核查节点:接入只读节点自己的配置(verify_model_spec),图级默认的接入是写作模型的。值是修复标题里的称呼
#:(「选择……的盲算模型改用的模型接入」);问题里说「……的「盲算模型 · 接入」指定的」,写检查器上那一栏的叫法
_VERIFY = {NodeType.DUAL_CHECK: "的盲算模型", NodeType.CHALLENGE: "的反方模型"}
def _name(value: Any) -> str | None:
return value.strip() or None if isinstance(value, str) else None
@dataclass(frozen=True)
class ProviderRef:
"""图里写着接入或模型的一处:落在哪个节点(全图默认为 None)、哪个字段,写的名字,句子里怎么称呼这一处,检查器里
有没有这一项的下拉框;kind 是写的接入(provider)还是模型(model)。
who 是修复标题里的称呼(「选择{who}改用的模型接入」);said 是「{said}指定的模型接入「X」」里的主语,默认同 who。
核查节点两者不同:指定接入的是「盲算模型 · 接入」这一栏,不是盲算模型本身。"""
node_id: str | None
field: str
name: str
who: str
editable: bool = False
kind: str = "provider"
said: str = ""
@property
def subject(self) -> str:
return self.said or self.who
def _place(node_id: str | None, field: str, value: Any, who: str, *, editable: bool = False,
kind: str = "provider", said: str = "") -> ProviderRef | None:
name = _name(value)
return ProviderRef(node_id, field, name, who, editable, kind, said) if name else None
@dataclass(frozen=True)
class Use:
"""执行时取一次模型:key 是(节点 id,哪一次:node、agents[i]、judge、verify),who 是句子里怎么称呼它;接入、模型
各取自哪一处(哪一级都没写为 None)。settings 是核查、裁判的接入和模型都没写、交给设置的那一项(verify / judge)。"""
key: tuple[str, str]
who: str
provider: ProviderRef | None
model: ProviderRef | None
settings: str | None = None
def _own_or_default(spec: GraphSpec, node: GraphNode, key: str, who: str) -> ProviderRef | None:
"""节点的 provider / model 取自哪一处,同 NodeContext.cfg:节点上没写(None、空串)就取全图默认。"""
if node.config.get(key) in (None, ""):
return _place(None, f"defaults.{key}", spec.defaults.get(key), field_label("defaults"), kind=key)
return _place(node.id, key, node.config.get(key), who, kind=key,
editable=key == "model" and node.type in _MODEL_PICKER)
def node_uses(spec: GraphSpec, node: GraphNode) -> list[Use]:
"""一个节点执行时取模型的每一处(uses 里属于它的那几项)。只读这个节点自己的配置和全图默认:改一个节点的配置
只影响它自己这几处,修复规划(autofix 的 provider.*)按这一点只重算被改的那个节点,不再每试一个候选就把整张图
重算一遍——几十个节点都点名同一个停用接入时,那样画布每改一下都要多算上万处。"""
out: list[Use] = []
cfg = node.config
who = f"「{node.title}」({type_label(node.type)})"
if node.type in _VERIFY:
# 接入、模型哪一项写了就整组以节点为准,都没写交给设置(verify_model_spec)
if cfg.get("enabled") is not False:
vwho = who + _VERIFY[node.type]
p = _place(node.id, "provider", cfg.get("provider"), vwho, editable=True,
said=f"{who}的「{field_label('provider', node.type)}」")
m = _place(node.id, "model", cfg.get("model"), vwho, editable=True, kind="model",
said=f"{who}的「{field_label('model', node.type)}」")
out.append(Use((node.id, "verify"), vwho, p, m, None if p or m else "verify"))
return out
if node.type not in _TAKES_MODEL:
return out
provider, model = _own_or_default(spec, node, "provider", who), _own_or_default(spec, node, "model", who)
cond = _TAKES_MODEL[node.type]
if cond is None or cond(spec, node):
out.append(Use((node.id, "node"), who, provider, model))
if node.type == NodeType.SUPERVISOR and isinstance(cfg.get("agents"), list):
# 成员自己写了(非空)就用自己的,否则跟随节点,同 nodes/multi._member 的 `cfg.get(...) or ctx.cfg(...)`
for i, member in enumerate(cfg["agents"]):
if not isinstance(member, dict):
continue
label = _name(member.get("name")) or f"第 {i + 1} 位"
mwho = f"{who}的成员「{label}」"
own_p, own_m = member.get("provider"), member.get("model")
out.append(Use((node.id, f"agents[{i}]"), mwho,
_place(node.id, f"agents[{i}].provider", own_p, mwho) if own_p else provider,
_place(node.id, f"agents[{i}].model", own_m, mwho, kind="model") if own_m else model))
if node.type == NodeType.REPORT and _cfg(spec, node, "claims", "off") == CLAIMS_JUDGE:
judge = _cfg(spec, node, "judge")
# 节点没写裁判配置,执行时用全图默认里的那一份:问题记在全图默认上,几个报告共用时只记一条
own = judge is cfg.get("judge")
nid, at, jwho = (node.id, "judge", f"{who}的{field_label('judge')}") if own else \
(None, "defaults.judge", f"{field_label('defaults')}中的{field_label('judge')}")
judge = judge if isinstance(judge, dict) else {}
p = _place(nid, f"{at}.provider", judge.get("provider"), jwho, editable=own)
m = _place(nid, f"{at}.model", judge.get("model"), jwho, editable=own, kind="model")
out.append(Use((node.id, "judge"), jwho, p, m, None if p or m else "judge"))
return out
def uses(spec: GraphSpec) -> list[Use]:
"""图里执行时取模型的每一处,按画布上节点的顺序;只算执行时真会取模型的(见模块说明)。"""
return [use for node in spec.nodes for use in node_uses(spec, node)]
def provider_refs(spec: GraphSpec) -> list[ProviderRef]:
"""执行时要查的那几处,按画布上节点的顺序,全图默认排最后,同一处只记一次:取到了接入的是那个接入;哪一级都没写
接入、只写了模型的是那个模型(按模型名找接入)。"""
found: dict[tuple[str | None, str], ProviderRef] = {}
for use in uses(spec):
if (ref := use.provider or use.model) is not None:
found.setdefault((ref.node_id, ref.field), ref)
return sorted(found.values(), key=lambda r: r.node_id is None)
def broken(refs: list[ProviderRef], providers: list[Any]) -> list[tuple[ProviderRef, ProviderNotConfigured]]:
"""执行时会报错的那几处,带着运行时报的那个错(code 是 missing / disabled):点名的接入按名字找(named_provider),
只写了模型的按模型名找(provider_for_model,认不出的模型名运行时放行,不算)。"""
out: list[tuple[ProviderRef, ProviderNotConfigured]] = []
for ref in refs:
try:
if ref.kind == "model":
provider_for_model(providers, ref.name)
else:
named_provider(providers, ref.name)
except ProviderNotConfigured as e:
out.append((ref, e))
return out
def _tail(spec: GraphSpec, ref: ProviderRef, providers: list[Any]) -> str:
"""提示的后半句:检查器里有这一项的下拉框,指到节点;没有的,发布前检查给得出修复才指到那里——一个候选都落不稳
时(比如一个启用的接入都没有,或者改动波及的别处都落不稳)那里什么都不给,指过去只会让人白找,改说卡在哪里。
只问给不给得出、卡在哪里(autofix.provider_fixable,找到第一个落得稳的候选就停),不拼整条修复。"""
if ref.editable:
return f",{_WHERE[ref.kind]}"
# 在这里导入:autofix 导入了门禁(governance),门禁又在 lint_for_publish 里导入本模块
from app.engine.autofix import provider_fixable
fixable, stuck = provider_fixable(spec, ref, providers)
if fixable:
return f",{IN_CHECK}"
return f"。{NO_SWITCH}{stuck}" if stuck else ""
def _model_note(spec: GraphSpec, ref: ProviderRef, e: ProviderNotConfigured, providers: list[Any]) -> str:
"""点名的名字恰好是某个启用接入的模型时补的那一句,按这一处在检查器里有哪几栏写(运行时报错、设置页的说法见
factory.model_note)。多数节点的检查器里没有接入这一栏,叫人「接入留空」做不到:
- 核查节点、报告自己的结论句裁判:接入、模型两栏都在(模型是输入框),照设置页的写法;
- 检查器里有模型下拉框、没有接入一栏的节点(模型调用、Agent……):在节点的「模型」中选它,接入在发布前检查里
改用这个接入或不指定(修复里有哪一项说哪一项);
- 协作成员、全图默认这些两栏都没有的:发布前检查里能改用这个接入时说一句,否则只说这是哪个接入的模型。"""
name, owner = e.name, e.model_owner
if ref.editable:
node = spec.node_map()[ref.node_id] if ref.node_id is not None else None
labels = (judge_label("model"), judge_label("provider")) if ref.field.startswith("judge.") or node is None \
else (field_label("model", node.type), field_label("provider", node.type))
return model_note(name, owner, f"「{labels[0]}」", f"「{labels[1]}」", "填写")
from app.engine.autofix import PROVIDER_CLEAR, provider_choice
fact = model_fact(name, owner)
choice = provider_choice(spec, ref, providers)
values = {o["value"] for o in choice["options"]} if choice else set()
node = spec.node_map().get(ref.node_id) if ref.node_id is not None else None
if node is not None and ref.field == "provider" and node.type in _MODEL_PICKER:
picks = ([f"改用「{owner}」"] if owner in values else []) + \
(["选择「不指定接入」"] if PROVIDER_CLEAR in values else [])
if not picks:
return fact
return f"{fact}:如需使用该模型,请在节点的「{field_label('model')}」中选择它,并在「发布前检查」中{'或'.join(picks)}"
return f"{fact},可在「发布前检查」中改用「{owner}」" if owner in values else fact
def issues(spec: GraphSpec, providers: list[Any], *, level: str) -> list[ValidationIssue]:
"""执行时会报错的每一处一条问题。providers 是接入表(factory.list_providers),level 由调用方定:画布校验 warning,
发布一类 error。原因和运行时报的同一句,提示按这一处在界面上能不能改(_tail);点名的名字恰好是某个启用接入的
模型时补一句(_model_note)。"""
out = []
for ref, e in broken(provider_refs(spec), providers):
note = f"。{_model_note(spec, ref, e, providers)}" if e.model_owner else ""
out.append(ValidationIssue(level=level, node_id=ref.node_id, field=ref.field, # type: ignore[arg-type]
code=f"provider.{e.code}",
message=f"{ref.subject}指定的{e.reason}。{NAMED_FIX[e.code]}"
f"{_tail(spec, ref, providers)}{note}"))
return out
def quiet_unset_model(spec: GraphSpec, found: list[Any], provider_issues: list[Any]) -> list[Any]:
"""去掉被接入问题盖住的「没有指定模型,将使用接入「X」的默认模型」(schema.MODEL_UNSET)。
节点没写模型、取到的接入(自己点名的,没写就是全图默认点名的,同 NodeContext.cfg)本身不存在或已停用时,那条说
会用 X 的默认模型照常执行,接入那一条说执行到这里会失败,读者无从判断哪条是真的。接入那一条带着原因和改法,留它。
found 是校验的问题(ValidationIssue),provider_issues 是 issues() 的结果;画布校验和发布一类各调一次。"""
hit = {(i.node_id, i.field) for i in provider_issues if i.code in CODES}
if not hit:
return found
nodes = spec.node_map()
def covered(issue: Any) -> bool:
if issue.code != MODEL_UNSET or (node := nodes.get(issue.node_id)) is None:
return False
own = node.config.get("provider") not in (None, "")
return ((node.id, "provider") if own else (None, "defaults.provider")) in hit
return [i for i in found if not covered(i)]
@dataclass(frozen=True)
class Landing:
"""执行时这一处落到哪里:where 是说给人听的(「工作流默认设置指定的接入「甲」」……),model 是用哪个模型(交给设置时
为 None),ok 是修复能不能把它落在这里。落到演示类接入时 demo 为真、where 末尾带着 DEMO_LANDING。落不稳时 why 说
为什么(一句话,接在「无法改用其他接入:」后面),修复一个候选都给不出时拿第一处的 why 告诉人卡在哪里。"""
where: str
model: str | None
ok: bool
demo: bool = False
why: str = ""
def land(use: Use, providers: list[Any]) -> Landing:
"""按执行器的取法(get_chat_model → resolve_provider → build_chat_model)算这一处执行时用哪个接入、哪个模型。
ok 为真才算落得稳:点名的接入启用着、有模型可用,并且提供这一处写的模型——拿 A 家的模型名去调 B 家必然失败;
只写了模型的,按模型名找到启用的接入。按模型名落到停用的接入(运行时报错)、模型名认不出来(落到第一个可用的
接入,它多半不提供)、一个启用的接入都没有(悄悄换成内置演示模型)都不算。
交给设置的(核查、裁判)算落得稳:设置保存时查过点名的接入,之后接入出了问题设置页会标出来。启用的接入全是演示类
时,设置这一级不管写的是什么,执行时落到的都是演示模型(点名的启用接入、按模型名找到的、取值链最后的第一个可用的
接入,只能是演示类),照实带上 DEMO_LANDING;一个启用的接入都没有时不算落得稳,同上面「换成内置演示模型」。"""
if use.settings:
enabled = [p for p in providers if getattr(p, "enabled", False)]
if not enabled:
return Landing(_SETTINGS[use.settings], None, False, why=NO_ENABLED)
demo = all(getattr(p, "kind", None) == "mock" for p in enabled)
return Landing(_SETTINGS[use.settings] + (DEMO_LANDING if demo else ""), None, True, demo=demo)
model = use.model.name if use.model is not None else None
if use.provider is not None:
try:
found = named_provider(providers, use.provider.name)
except ProviderNotConfigured as e:
return Landing(e.reason, model, False, why=f"{use.provider.subject}指定的{e.reason}")
where = f"{use.provider.subject}指定的接入「{found.name}」"
elif use.model is not None and model:
try:
found = provider_for_model(providers, model)
except ProviderNotConfigured as e:
return Landing(e.reason, model, False, why=f"{use.model.subject}指定的{e.reason}")
if found is None:
return Landing(f"认不出的模型「{model}」", model, False,
why=f"{use.model.subject}指定的模型「{model}」不属于任何启用的模型接入")
where = f"提供模型「{model}」的接入「{found.name}」"
else:
found = fallback_provider(providers)
if found is None:
return Landing("内置演示模型", None, False, why=NO_ENABLED)
where = f"第一个可用的模型接入「{found.name}」"
demo = getattr(found, "kind", None) == "mock"
if demo:
where += DEMO_LANDING
used = model or found.default_model
if not used:
return Landing(where, None, False, demo, why=f"模型接入「{found.name}」没有可用的模型")
if use.model is not None and model and not serves(found, model):
return Landing(where, model, False, demo, why=f"模型接入「{found.name}」不提供{use.model.subject}指定的模型「{model}」")
return Landing(where, used, True, demo)
async def load_issues(session: AsyncSession, spec: GraphSpec, *, level: str) -> list[ValidationIssue]:
"""读接入表、查一遍。图里执行时取模型的几处都没写接入和模型时不读库。"""
if not provider_refs(spec):
return []
return issues(spec, await list_providers(session), level=level)
async def load_providers(session: AsyncSession, spec: GraphSpec) -> list[Any] | None:
"""发布、发布前检查、自动修复交给门禁的接入表(governance.lint_for_publish 的 providers)。图里执行时取模型的几处
都没写接入和模型时不读库,给空表:没有要查的,修复也用不上候选。"""
return await list_providers(session) if provider_refs(spec) else []