package billcost import ( "context" "encoding/json" "fmt" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // ReflectTool wraps billcost.Reflect as a tools.Tool so an engine // Session can register it and call it via the model's tool-use // channel. PM design: the bill-recon main flow is the master agent; // quote-table extraction is a sub-tool (LLM call); reflector is // another sub-tool (this one). Wrapping Reflect as a Tool lets the // future C6+ engine.Session-driven path call it the same way the // current direct-call path does, with no schema drift. // // ReflectTool 把 billcost.Reflect 包装成 tools.Tool, 让 engine.Session // 可注册并经模型 tool-use 通道调用. PM 设计: bill-recon 主流程是主 // agent; 报价表抽取是子工具 (LLM 调用); 反射器是另一子工具 (本工具). // 包成 Tool 让未来 C6+ engine.Session 驱动路径与当前直调路径用同一 // schema, 不漂移. type ReflectTool struct { // Cfg is the ReflectConfig the tool applies on every call. Pass // the same config the workflow's reflectQuoteOutput uses so // engine-driven and direct-call paths agree. // // Cfg 工具每次调用使用的 ReflectConfig. 与 workflow.reflectQuoteOutput // 的 cfg 同款, 让 engine-driven / 直调两条路径一致. Cfg ReflectConfig } // reflectToolInput is the JSON input the model sends. Output is a // billcost.Output payload (the LLM's parsed quote table); the model // may also override the config when it wants different bounds, but // most callers leave it nil and let the tool use Cfg. // // reflectToolInput 模型发送的 JSON 输入. Output 是 billcost.Output // (LLM 已解析的报价表); 模型可选地覆盖 config (一般留 nil 用 Cfg). type reflectToolInput struct { Output Output `json:"output"` Config *ReflectConfig `json:"config,omitempty"` } // reflectToolOutput is the structured result returned to the model. // Violations is the slice billcost.Reflect produced; ViolationCount // duplicates len() so the model can branch on it without parsing. // // reflectToolOutput 返回给模型的结构化结果. Violations 是 // billcost.Reflect 产物; ViolationCount 重复 len() 让模型不必解析就 // 能 branch. type reflectToolOutput struct { Violations []Violation `json:"violations"` ViolationCount int `json:"violation_count"` Pass bool `json:"pass"` } // Name returns the stable tool identifier "billcost_reflect". // // CLEVER (Bug U, 2026-05-01): underscore separator, not dot. OpenAI // function-calling protocol enforces regex ^[a-zA-Z0-9_-]+$ on // function names; OpenRouter forwards to underlying providers // (SiliconFlow / DeepSeek / etc.) that validate strictly and return // HTTP 400 for any dot in the name. Anthropic tool_use is just as // strict. Underscore is the canonical separator across the LLM // tool-calling ecosystem. // // Name 返回稳定工具标识 "billcost_reflect". // // CLEVER (Bug U, 2026-05-01): 下划线分隔, 不用点. OpenAI // function-calling 协议对 function name 强制 regex ^[a-zA-Z0-9_-]+$; // OpenRouter 转给底层 provider (SiliconFlow / DeepSeek 等) 严格校验, // dot 立即 HTTP 400. Anthropic tool_use 同款严格. 下划线是 LLM // tool-calling 生态通用分隔符. func (r *ReflectTool) Name() string { return "billcost_reflect" } // Description returns the model-facing help text. The model uses it // to decide when to call the tool, so it states the input shape + // what a non-empty violations list means + when to call again // after fixing. // // Description 返回面向模型的帮助文本. 模型据此决定何时调用此工具, // 所以描述输入形状 + 非空 violations 的含义 + 修正后何时再调. func (r *ReflectTool) Description(ctx context.Context) string { return `Validate a parsed shipping cost table (billcost.Output) against the WMS-schema invariants. Input: {"output": billcost.Output, "config": optional ReflectConfig overrides}. Returns: {"violations": [...], "violation_count": int, "pass": bool}. When pass=false, fix the listed issues in the next quote-extraction call. Each violation carries a rule_id (e.g. "first_lt_increment", "segment_gap"), the partition (province) it occurred in, the sample weight that triggered it (when applicable), and a human-readable detail. Re-call this tool after fixing to confirm convergence. The tool itself is a pure deterministic dry-run -- no LLM, no DB access. Safe to call repeatedly; no side effects.` } // reflectInputSchema is the JSON Schema the model uses to generate // the input. Kept inline as a constant string so InputSchema() // returns it without per-call marshalling. // // reflectInputSchema 是模型用来生成输入的 JSON Schema. 内联常量字符串 // 让 InputSchema() 不必每次调用 marshalling. const reflectInputSchema = `{ "type": "object", "properties": { "output": { "type": "object", "description": "Parsed quote table (billcost.Output schema): bands + strip_fees + return_fee." }, "config": { "type": "object", "description": "Optional ReflectConfig overrides; omit to use the tool's defaults." } }, "required": ["output"] }` // InputSchema returns the model-facing JSON schema. // // InputSchema 返回面向模型的 JSON schema. func (r *ReflectTool) InputSchema() json.RawMessage { return json.RawMessage(reflectInputSchema) } // Execute runs Reflect against the parsed output. Returns a // tools.Result with structured Data + a human-readable Output. The // Output text is what the model sees in the next turn; Data is what // any downstream programmatic consumer (engine orchestrator, // audit sink) reads. // // Execute 跑 Reflect. 返 tools.Result 含结构化 Data + 人类可读 Output. // Output 文本是模型下一轮看到的内容; Data 给下游程序消费方 (引擎编排 // / audit sink) 读. func (r *ReflectTool) Execute(ctx context.Context, input json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { var in reflectToolInput if err := json.Unmarshal(input, &in); err != nil { return &tools.Result{ Output: fmt.Sprintf("billcost_reflect: invalid input json: %v", err), IsError: true, }, nil } cfg := r.Cfg if in.Config != nil { cfg = *in.Config } violations := Reflect(in.Output, cfg) res := reflectToolOutput{ Violations: violations, ViolationCount: len(violations), Pass: len(violations) == 0, } humanText := summarizeViolations(violations) return &tools.Result{ Output: humanText, Data: res, }, nil } // Metadata declares the tool as concurrency-safe + read-only + // non-destructive. Reflect has no side effects so it satisfies all // three. PermissionClass="readonly" lets the engine permission // chain admit it without prompting. // // Metadata 声明工具并发安全 + 只读 + 非破坏性. Reflect 无副作用全部 // 满足. PermissionClass="readonly" 让引擎权限链不弹窗. func (r *ReflectTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: true, ReadOnly: true, Destructive: false, SearchHint: "billcost reflect dry-run", PermissionClass: "readonly", } } // summarizeViolations renders the violation list as a short string // the LLM can read in the next turn. Empty list returns "pass" so // the model knows it converged. // // summarizeViolations 渲染 violation 列表为短字符串供 LLM 下轮看. 空 // 列表返 "pass" 让模型知道收敛. func summarizeViolations(violations []Violation) string { if len(violations) == 0 { return "pass: 0 violations" } out := fmt.Sprintf("fail: %d violations\n", len(violations)) for i, v := range violations { if i >= 10 { out += fmt.Sprintf("... %d more\n", len(violations)-i) break } out += fmt.Sprintf("- [%s] %s\n", v.RuleID, v.Detail) } return out } // Compile-time check ReflectTool satisfies tools.Tool. // // 编译期检查 ReflectTool 实现 tools.Tool. var _ tools.Tool = (*ReflectTool)(nil)