the-mindscape-of-bro-tsong/scripts/run_selector_demo.py

243 lines
9.8 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.

import json
import sys
from pathlib import Path
def read_json(path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def load_models(root):
return [read_json(path) for path in sorted((root / "models").glob("*.model.json"))]
def load_selector_rules(root):
return read_json(root / "selector" / "selector_rules.json")
def rules_by_model(selector_rules):
return {item["model_id"]: item for item in selector_rules.get("models", [])}
def hit_any(user_input, signals):
return [signal for signal in signals if signal in user_input]
def instruction_segment(user_input):
segments = [user_input]
for delimiter in ("", ":", "\n"):
if delimiter in user_input:
segments.append(user_input.split(delimiter, 1)[0])
return min(segments, key=len)
def has_analysis_override(user_input, global_rules):
return bool(hit_any(user_input, global_rules.get("explicit_analysis_override_phrases", [])))
def hard_no_call_hits(user_input, global_rules):
signals = list(global_rules.get("hard_no_call_signals", []))
signals.extend(global_rules.get("direct_execution_no_call_signals", []))
hits = hit_any(user_input, signals)
instruction_hits = instruction_task_no_call_hits(
user_input,
global_rules.get("instruction_task_no_call_signals", []),
)
return hits + instruction_hits
def instruction_task_no_call_hits(user_input, signals):
instruction = instruction_segment(user_input)
if instruction != user_input:
return hit_any(instruction, signals)
stripped = user_input.strip()
return [
signal
for signal in signals
if 0 <= stripped.find(signal) <= 4
]
def depth_limited_qpi_override_hits(user_input, global_rules):
depth_hits = hit_any(user_input, global_rules.get("depth_limiting_no_call_signals", []))
override_hits = hit_any(user_input, global_rules.get("qpi_limited_analysis_override_phrases", []))
return depth_hits, override_hits
def model_hard_exclusion_hits(user_input, model_rule):
return hit_any(user_input, model_rule.get("hard_exclusion_triggers", []))
def score_model(model, model_rule, global_rules, user_input, task_type="", pipeline_position=""):
model_id = model.get("model_id")
weights = global_rules.get("weights", {})
score = float(model_rule.get("base_score", 0)) / 100
reasons = []
penalties = []
negative_hits = hit_any(user_input, model_rule.get("negative_triggers", [])) + hit_any(user_input, model.get("negative_triggers", []))
depth_hits, qpi_limited_hits = depth_limited_qpi_override_hits(user_input, global_rules)
suppress_qpi_depth_penalty = model_id == "qpi" and depth_hits and qpi_limited_hits
if negative_hits and not has_analysis_override(user_input, global_rules) and not suppress_qpi_depth_penalty:
penalty_key = "model_negative_penalty_qpi" if model_id == "qpi" else "model_negative_penalty_default"
score -= weights.get(penalty_key, 0.5)
penalties.append("negative trigger: " + "".join(sorted(set(negative_hits))[:4]))
trigger_hits = hit_any(user_input, model_rule.get("trigger_keywords", [])) + hit_any(user_input, model.get("trigger_keywords", []))
trigger_hits = sorted(set(trigger_hits), key=trigger_hits.index)
if trigger_hits:
score += weights.get("trigger_keyword", 0.25)
reasons.append("trigger keyword matched: " + "".join(trigger_hits[:4]))
input_type_matches = model_rule.get("input_type_matches", []) + model.get("input_types", [])
input_hits = [input_type for input_type in input_type_matches if input_type in user_input or input_type == task_type]
if input_hits:
score += weights.get("input_type", 0.15)
reasons.append("input type matched: " + "".join(input_hits[:3]))
if pipeline_position and model.get("pipeline_position") == pipeline_position:
score += weights.get("pipeline_position", 0.2)
reasons.append("pipeline_position matched")
complexity_hits = hit_any(user_input, global_rules.get("complexity_signals", []))
if complexity_hits:
score += weights.get("complexity_signal", 0.15)
reasons.append("complexity signal: " + "".join(complexity_hits[:4]))
priority = model.get("selection_priority")
if isinstance(priority, int):
score += max(0, min(priority, 10)) * weights.get("selection_priority_factor", 0.01)
reasons.append(f"selection_priority={priority}")
qpi_gate_hits = hit_any(user_input, global_rules.get("qpi_gate_signals", []))
if model_id == "qpi" and qpi_gate_hits:
score += weights.get("qpi_gate_bonus", 0.25)
reasons.append("QPI problem-definition gate matched")
if model_id == "qpi":
has_positive_signal = bool(
trigger_hits
or input_hits
or complexity_hits
or qpi_gate_hits
or has_analysis_override(user_input, global_rules)
or task_type in {"problem_definition", "question_analysis"}
or pipeline_position in {"pre_analysis", "problem_definition"}
)
if not has_positive_signal:
score -= weights.get("qpi_missing_positive_signal_penalty", 0.25)
penalties.append("QPI positive signal missing")
if model_id == "qpi" and "QPI 已判断" in user_input:
score -= weights.get("qpi_already_completed_penalty", 0.25)
penalties.append("QPI already completed")
if model_id == "intellectual_archaeology":
heavy_hits = hit_any(user_input, global_rules.get("ia_heavy_signals", []))
if heavy_hits:
score += weights.get("ia_heavy_bonus", 0.2)
reasons.append("IA heavy-depth signal: " + "".join(heavy_hits[:3]))
if "QPI 已判断" in user_input:
score += weights.get("ia_qpi_completed_bonus", 0.25)
reasons.append("QPI-before-IA gate satisfied")
if global_rules.get("qpi_before_ia_gate", True) and qpi_gate_hits and "QPI 已判断" not in user_input:
score -= weights.get("ia_qpi_gate_penalty", 0.45)
penalties.append("QPI-before-IA gate: " + "".join(qpi_gate_hits[:3]))
if not heavy_hits and "QPI 已判断" not in user_input:
score -= weights.get("ia_no_heavy_gate_penalty", 0.2)
penalties.append("no IA heavy-depth gate")
score = min(max(score, 0.0), 1.0)
return {
"model_id": model_id,
"score": round(score, 2),
"reasons": reasons,
"penalties": penalties
}
def hard_no_call_result(root, user_input, hits):
rejected = []
for model in load_models(root):
rejected.append({
"model_id": model.get("model_id"),
"score": 0.0,
"reasons": [],
"penalties": ["hard no-call signal: " + "".join(sorted(set(hits))[:4])],
"decision": "rejected"
})
return {
"input": user_input,
"selected_models": [],
"rejected_models": rejected,
"no_call": True,
"routing_notes": "hard no-call gate matched; explicit analysis override was not present."
}
def recommend(root, user_input, task_type="", pipeline_position="", threshold=None):
selector_rules = load_selector_rules(root)
global_rules = selector_rules.get("global_rules", {})
threshold = global_rules.get("no_call_threshold", 0.35) if threshold is None else threshold
no_call_hits = hard_no_call_hits(user_input, global_rules)
depth_hits, qpi_limited_hits = depth_limited_qpi_override_hits(user_input, global_rules)
has_depth_limited_qpi_override = bool(depth_hits and qpi_limited_hits)
if (
global_rules.get("hard_no_call_first", True)
and no_call_hits
and not has_analysis_override(user_input, global_rules)
and not has_depth_limited_qpi_override
):
return hard_no_call_result(root, user_input, no_call_hits)
model_rules = rules_by_model(selector_rules)
scored = []
for model in load_models(root):
model_id = model.get("model_id")
model_rule = model_rules.get(model_id, {})
item = score_model(model, model_rule, global_rules, user_input, task_type, pipeline_position)
exclusion_hits = model_hard_exclusion_hits(user_input, model_rule)
if exclusion_hits:
item["score"] = 0.0
item["hard_excluded"] = True
item["penalties"].append("model hard exclusion signal: " + "".join(sorted(set(exclusion_hits))[:4]))
scored.append(item)
no_call = all(item["score"] < threshold for item in scored)
selected = []
rejected = []
for item in scored:
output_item = {
"model_id": item["model_id"],
"score": item["score"],
"reasons": item["reasons"],
"penalties": item["penalties"],
"decision": "selected" if item["score"] >= threshold and not no_call and not item.get("hard_excluded") else "rejected"
}
if output_item["decision"] == "selected":
selected.append(output_item)
else:
rejected.append(output_item)
selected.sort(key=lambda item: item["score"], reverse=True)
rejected.sort(key=lambda item: item["score"], reverse=True)
return {
"input": user_input,
"selected_models": selected,
"rejected_models": rejected,
"no_call": no_call,
"routing_notes": "selector is rule-based and driven by selector/selector_rules.json; no LLM, no vector search, no answer generation."
}
def main():
root = Path(__file__).resolve().parents[1]
example = "团队每次都说要长期主义,但一到季度 KPI 就回到短期动作,这到底是什么问题?"
result = recommend(root, example, task_type="question_analysis", pipeline_position="pre_analysis")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())