feat(model,capacity,vision,io): per-persona 路由 + 本地三级(默认关) + L3 分类 + OCR 独立模型 + image-crop 工具(裁决 03.4.1/03.4.3/14.4.1/14.4.4/14.4.5)
- 03.4.1:per-persona 路由载体选 models.json(组内新增 personaModels/personaProviders + 校验 + PersonaRoute()), Router/RuntimeLLM 快照绑定、loop 流式路径按该 turn 的 persona 路由;主步用路由结果、降级步钉自己的模型 - 03.4.3:本地第三级降级链(LLM_LOCAL_*,Configured() 含 Enabled ⇒ **默认关**),warn 标注来源 - 14.4.4:capacity 接入 L3(分类器接口 + TTL 缓存 + 严格 JSON 解析),CAPACITY_L3_ENABLED **默认 false** - 14.4.1:语义修正 —— group.multimodal 才是「主模型能否收图」(env 路径不再臆断),LLM_VISION_MODEL 只配视觉工具; vision/OCR 可各用不同模型;runtime 不再因有图抢路由(vision.Router 是唯一分层权威) - 14.4.5:新增 image-crop 工具(工具数 5→6,策略默认 owner-only),schema 支持按区域裁切 - 组合根:wire_runtime 读 group.Multimodal;capacity 装配拆出 wire_capacity.go(守 500 行) - 复核:`go build`/`go vet`/各包 `go test -count=1` 全绿;**6 条变异反证**先红后还原(含作者自查出的 2 条假阳性变异) - 未落:按 provider 的 thinking 开关(需 openai_encode.go/openai.go 文件面)、persona awakening(需 loader 文件面)
This commit is contained in:
1 parent
63e5b70e3d
commit
fa33c79e11
24 files changed
+1941
-122
No files matched your search
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
|
||||
)
|
||||
|
||||
// 本文件拥有唤醒式能力(capacity)决策层的装配:判定器构造、工具集 JSON 解析、
|
||||
// L3 缓存 TTL 解析与 capacity_awaken 事件出口。它从 wire_runtime.go 拆出
|
||||
// (AGENTS §10 文件 ≤500 行;wire.go 拆分建议同样适用于本文件)。
|
||||
|
||||
// buildCapacity constructs the awakened-capacity decider when enabled, or nil
|
||||
// (which keeps the loop's default all-tools behavior). It is opt-in via
|
||||
// CAPACITY_ENABLED; the allowed-set mapping comes from CAPACITY_TOOL_SETS and
|
||||
// falls back to built-in defaults on a parse error.
|
||||
//
|
||||
// The model-assisted L3 layer (裁决 14.4.4) is a second, independent switch
|
||||
// (CAPACITY_L3_ENABLED, default false): even with capacity on, L3 stays silent
|
||||
// unless it was explicitly asked for, and its verdict cache TTL
|
||||
// (CAPACITY_L3_CACHE_TTL, default 5m) is parsed here because the config loader
|
||||
// only carries scalars.
|
||||
func (a *app) buildCapacity() *capacity.Decider {
|
||||
if !a.cfg.Capacity.Enabled {
|
||||
return nil
|
||||
}
|
||||
sets, err := parseCapacityToolSets(a.cfg.Capacity.ToolSetsJSON)
|
||||
if err != nil {
|
||||
a.log.Warn("capacity tool sets parse failed, using built-in defaults", "error", err)
|
||||
sets = nil
|
||||
}
|
||||
cfg := capacity.Config{
|
||||
Enabled: true,
|
||||
MinConfidence: a.cfg.Capacity.MinConfidence,
|
||||
ToolSets: sets,
|
||||
}
|
||||
if a.cfg.Capacity.L3Enabled {
|
||||
cfg.L3Enabled = true
|
||||
cfg.L3Classifier = capacity.NewLLMClassifier(a.modelAdapter, a.log)
|
||||
cfg.L3CacheTTL = parseL3CacheTTL(a.cfg.Capacity.L3CacheTTL, a.log)
|
||||
}
|
||||
return capacity.New(cfg, capacity.WithLogger(a.log))
|
||||
}
|
||||
|
||||
// parseL3CacheTTL parses the CAPACITY_L3_CACHE_TTL duration string. An empty
|
||||
// value selects the code default (5m, the landing suggestion of 裁决 14.4.4); an
|
||||
// unparsable value is reported and falls back to the same default rather than
|
||||
// silently disabling the cache.
|
||||
func parseL3CacheTTL(raw string, log *slog.Logger) time.Duration {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return capacity.DefaultL3CacheTTL
|
||||
}
|
||||
d, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
logger.WithSource(log, "cmd/cyrene").Warn("capacity L3 cache ttl unparsable, using the default",
|
||||
"value", raw, "default", capacity.DefaultL3CacheTTL.String(), "error", err)
|
||||
return capacity.DefaultL3CacheTTL
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// parseCapacityToolSets decodes the CAPACITY_TOOL_SETS JSON map of
|
||||
// state→allowed tool name list. A blank string yields nil (built-in defaults); a
|
||||
// null value for a state means unrestricted (all tools).
|
||||
func parseCapacityToolSets(raw string) (map[capacity.State][]string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var m map[string][]string
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return nil, fmt.Errorf("capacity tool sets: %w", err)
|
||||
}
|
||||
out := make(map[capacity.State][]string, len(m))
|
||||
for k, v := range m {
|
||||
out[capacity.State(k)] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// onCapacity is the loop's OnCapacity sink: it observes a capacity_awaken
|
||||
// transition and pushes it to the Web UI (design/14 §5) as a `capacity_awaken`
|
||||
// frame. The loop only emits on a real state transition (never every round), and
|
||||
// the event carries the originating session id so it routes to that session, not
|
||||
// globally. The payload is the frontend AwakenPayload {fromState,toState,tool}.
|
||||
func (a *app) onCapacity(ev capacity.Event) {
|
||||
logger.WithSource(a.log, "cmd/cyrene").Debug("capacity event",
|
||||
"type", ev.Type, "from", ev.FromState, "to", ev.ToState,
|
||||
"tool", ev.Tool, "at", ev.At, "session", ev.SessionID)
|
||||
if a.capacitySink == nil {
|
||||
return
|
||||
}
|
||||
a.capacitySink.BroadcastCapacityAwaken(ev.SessionID, string(ev.FromState), string(ev.ToState), ev.Tool)
|
||||
}
|
||||
+13
-56
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
|
||||
personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
@@ -191,8 +189,14 @@ func (a *app) wireRuntime() error {
|
||||
}
|
||||
|
||||
// Vision routing: the loop rewrites an image-bearing user message per the
|
||||
// vision Router — a vision-capable main model sees the image directly,
|
||||
// otherwise the vision/OCR tool's text description replaces it (design/06 §0).
|
||||
// vision Router — a main model that really accepts image input sees the image
|
||||
// directly, otherwise the vision/OCR tool's text description replaces it
|
||||
// (design/06 §0)。
|
||||
//
|
||||
// SupportsVision means "主模型能否收图"(裁决 14.4.1 修语义,C-7), read from the
|
||||
// active models.json group's `multimodal` declaration — NOT from
|
||||
// LLM_VISION_MODEL, which configures the vision *tool's* model. It is read per
|
||||
// call so a panel save/reload takes effect on the next turn.
|
||||
visionRouter := vision.NewRouter(
|
||||
vision.WithVisionTool(visionToolName),
|
||||
vision.WithOCRTool(ocrToolName),
|
||||
@@ -200,7 +204,8 @@ func (a *app) wireRuntime() error {
|
||||
visionResolver := vision.NewMessageResolver(
|
||||
visionRouter,
|
||||
func() vision.ModelInfo {
|
||||
return vision.ModelInfo{Name: a.cfg.LLM.VisionModel, SupportsVision: a.cfg.LLM.VisionModel != ""}
|
||||
group := a.llm.Group()
|
||||
return vision.ModelInfo{Name: group.Model, SupportsVision: group.Multimodal}
|
||||
},
|
||||
a.dispatcher.Invoke,
|
||||
)
|
||||
@@ -374,40 +379,8 @@ func (a *app) approvalActor(ctx context.Context) string {
|
||||
return pid
|
||||
}
|
||||
|
||||
// buildCapacity constructs the awakened-capacity decider when enabled, or nil
|
||||
// (which keeps the loop's default all-tools behavior). It is opt-in via
|
||||
// CAPACITY_ENABLED; the allowed-set mapping comes from CAPACITY_TOOL_SETS and
|
||||
// falls back to built-in defaults on a parse error.
|
||||
func (a *app) buildCapacity() *capacity.Decider {
|
||||
if !a.cfg.Capacity.Enabled {
|
||||
return nil
|
||||
}
|
||||
sets, err := parseCapacityToolSets(a.cfg.Capacity.ToolSetsJSON)
|
||||
if err != nil {
|
||||
a.log.Warn("capacity tool sets parse failed, using built-in defaults", "error", err)
|
||||
sets = nil
|
||||
}
|
||||
return capacity.New(capacity.Config{
|
||||
Enabled: true,
|
||||
MinConfidence: a.cfg.Capacity.MinConfidence,
|
||||
ToolSets: sets,
|
||||
}, capacity.WithLogger(a.log))
|
||||
}
|
||||
|
||||
// onCapacity is the loop's OnCapacity sink: it observes a capacity_awaken
|
||||
// transition and pushes it to the Web UI (design/14 §5) as a `capacity_awaken`
|
||||
// frame. The loop only emits on a real state transition (never every round), and
|
||||
// the event carries the originating session id so it routes to that session, not
|
||||
// globally. The payload is the frontend AwakenPayload {fromState,toState,tool}.
|
||||
func (a *app) onCapacity(ev capacity.Event) {
|
||||
logger.WithSource(a.log, "cmd/cyrene").Debug("capacity event",
|
||||
"type", ev.Type, "from", ev.FromState, "to", ev.ToState,
|
||||
"tool", ev.Tool, "at", ev.At, "session", ev.SessionID)
|
||||
if a.capacitySink == nil {
|
||||
return
|
||||
}
|
||||
a.capacitySink.BroadcastCapacityAwaken(ev.SessionID, string(ev.FromState), string(ev.ToState), ev.Tool)
|
||||
}
|
||||
// buildCapacity / onCapacity / capacity JSON+TTL parsing live in
|
||||
// wire_capacity.go (AGENTS §10 文件 ≤500 行).
|
||||
|
||||
// onToolAudit is the loop's OnAudit sink: it forwards a tool call denied by the
|
||||
// design/11 §3 security gate in a group/stranger environment into the gateway
|
||||
@@ -424,23 +397,7 @@ func (a *app) onToolAudit(ev loop.AuditEvent) {
|
||||
a.gw.AuditEvent(ev.Who, ev.What, ev.Result)
|
||||
}
|
||||
|
||||
// parseCapacityToolSets decodes the CAPACITY_TOOL_SETS JSON map of
|
||||
// state→allowed tool name list. A blank string yields nil (built-in defaults); a
|
||||
// null value for a state means unrestricted (all tools).
|
||||
func parseCapacityToolSets(raw string) (map[capacity.State][]string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var m map[string][]string
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return nil, fmt.Errorf("capacity tool sets: %w", err)
|
||||
}
|
||||
out := make(map[capacity.State][]string, len(m))
|
||||
for k, v := range m {
|
||||
out[capacity.State(k)] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
// parseCapacityToolSets / parseL3CacheTTL live in wire_capacity.go.
|
||||
|
||||
// dbCfgForEmbed returns the DB config used by the vector index, which opens the
|
||||
// same SQLite file as the primary pool.
|
||||
|
||||
Reference in new issue
Block a user