firefly-judgment 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. firefly/__init__.py +6 -0
  2. firefly/bootstrap.py +38 -0
  3. firefly/bus/__init__.py +17 -0
  4. firefly/bus/audit.py +90 -0
  5. firefly/bus/bus.py +249 -0
  6. firefly/bus/cache.py +55 -0
  7. firefly/bus/fallback.py +106 -0
  8. firefly/bus/metrics.py +67 -0
  9. firefly/core/__init__.py +13 -0
  10. firefly/core/calibrator.py +77 -0
  11. firefly/core/encoder.py +126 -0
  12. firefly/core/firefly_core.py +151 -0
  13. firefly/core/heads.py +96 -0
  14. firefly/core/scorer.py +192 -0
  15. firefly/evolution/__init__.py +54 -0
  16. firefly/evolution/ab.py +130 -0
  17. firefly/evolution/change_detector.py +227 -0
  18. firefly/evolution/data_synthesizer.py +202 -0
  19. firefly/evolution/discovery.py +247 -0
  20. firefly/evolution/engine.py +496 -0
  21. firefly/evolution/forgetter.py +51 -0
  22. firefly/evolution/publisher.py +273 -0
  23. firefly/evolution/replay.py +87 -0
  24. firefly/evolution/samples.py +58 -0
  25. firefly/evolution/shadow.py +111 -0
  26. firefly/evolution/trainer.py +112 -0
  27. firefly/graph/__init__.py +4 -0
  28. firefly/graph/tool_graph.py +187 -0
  29. firefly/nodes/__init__.py +16 -0
  30. firefly/nodes/base.py +143 -0
  31. firefly/nodes/gate.py +207 -0
  32. firefly/nodes/loop.py +115 -0
  33. firefly/nodes/memory.py +136 -0
  34. firefly/nodes/meta.py +119 -0
  35. firefly/nodes/router.py +64 -0
  36. firefly/nodes/schedule.py +76 -0
  37. firefly/nodes/verify.py +99 -0
  38. firefly/protocol/__init__.py +38 -0
  39. firefly/protocol/models.py +469 -0
  40. firefly/protocol/schemas/decision_log.schema.json +24 -0
  41. firefly/protocol/schemas/decision_request.schema.json +100 -0
  42. firefly/protocol/schemas/decision_response.schema.json +74 -0
  43. firefly/protocol/schemas/tool_spec.schema.json +42 -0
  44. firefly/protocol/schemas.py +27 -0
  45. firefly/protocol/validation.py +89 -0
  46. firefly/protocol/version.py +5 -0
  47. firefly/sdk/__init__.py +4 -0
  48. firefly/sdk/client.py +160 -0
  49. firefly/server/__init__.py +4 -0
  50. firefly/server/http.py +124 -0
  51. firefly_judgment-0.1.0.dist-info/METADATA +276 -0
  52. firefly_judgment-0.1.0.dist-info/RECORD +56 -0
  53. firefly_judgment-0.1.0.dist-info/WHEEL +5 -0
  54. firefly_judgment-0.1.0.dist-info/entry_points.txt +2 -0
  55. firefly_judgment-0.1.0.dist-info/licenses/LICENSE +21 -0
  56. firefly_judgment-0.1.0.dist-info/top_level.txt +1 -0
firefly/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """萤 (Firefly) —— 可嵌入 Agent Harness 的自成长判断系统。
2
+
3
+ P0 核心包:协议 / 萤核 / 节点 / 工具图谱 / 总线 / SDK / 边车。
4
+ """
5
+
6
+ __version__ = "0.1.0"
firefly/bootstrap.py ADDED
@@ -0,0 +1,38 @@
1
+ """一键装配萤总线:P0 路由/守门双节点;swarm_nodes=True 时加 P2 萤群五节点。"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable, Optional
5
+
6
+ from .bus import DecisionCache, Fallback, FireflyBus
7
+ from .nodes import (
8
+ GateNode,
9
+ GuardPolicy,
10
+ LoopNode,
11
+ MemoryNode,
12
+ MetaNode,
13
+ RouterNode,
14
+ ScheduleNode,
15
+ VerifyNode,
16
+ )
17
+
18
+
19
+ def build_bus(
20
+ llm_decider: Optional[Callable] = None,
21
+ user_decider: Optional[Callable] = None,
22
+ cache_ttl_seconds: float = 5.0,
23
+ gate_policy: Optional[GuardPolicy] = None,
24
+ swarm_nodes: bool = False,
25
+ ) -> FireflyBus:
26
+ bus = FireflyBus(
27
+ cache=DecisionCache(ttl_seconds=cache_ttl_seconds),
28
+ fallback=Fallback(llm_decider=llm_decider, user_decider=user_decider),
29
+ )
30
+ bus.register_node(RouterNode())
31
+ bus.register_node(GateNode(policy=gate_policy))
32
+ if swarm_nodes:
33
+ bus.register_node(VerifyNode())
34
+ bus.register_node(LoopNode())
35
+ bus.register_node(ScheduleNode())
36
+ bus.register_node(MemoryNode())
37
+ bus.register_node(MetaNode())
38
+ return bus
@@ -0,0 +1,17 @@
1
+ """萤总线层:路由 / 缓存 / 回退 / 审计 / 指标。"""
2
+ from .audit import AuditLog
3
+ from .bus import (
4
+ FireflyBus,
5
+ FireflyError,
6
+ NodeNotFound,
7
+ ProtocolViolation,
8
+ )
9
+ from .cache import DecisionCache
10
+ from .fallback import Fallback, FallbackDecision
11
+ from .metrics import Metrics
12
+
13
+ __all__ = [
14
+ "FireflyBus", "FireflyError", "NodeNotFound", "ProtocolViolation",
15
+ "DecisionCache", "Fallback", "FallbackDecision",
16
+ "AuditLog", "Metrics",
17
+ ]
firefly/bus/audit.py ADDED
@@ -0,0 +1,90 @@
1
+ """决策审计日志(方案 5.6 / 9.1:审计日志不可篡改)。
2
+
3
+ 每条日志包含前一条哈希,形成 SHA-256 哈希链;可导出 JSONL 并随时验链。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import threading
10
+ from typing import Dict, List, Optional
11
+
12
+ from ..protocol.models import DecisionLogEntry
13
+
14
+
15
+ class AuditLog:
16
+ def __init__(self) -> None:
17
+ self._entries: Dict[str, DecisionLogEntry] = {}
18
+ self._order: List[str] = []
19
+ self._prev_hash: Optional[str] = None
20
+ self._lock = threading.RLock()
21
+
22
+ def append(self, entry: DecisionLogEntry) -> DecisionLogEntry:
23
+ with self._lock:
24
+ entry.prev_hash = self._prev_hash
25
+ entry.entry_hash = self._hash(entry)
26
+ self._entries[entry.trace_id] = entry
27
+ self._order.append(entry.trace_id)
28
+ self._prev_hash = entry.entry_hash
29
+ return entry
30
+
31
+ def get(self, trace_id: str) -> Optional[DecisionLogEntry]:
32
+ with self._lock:
33
+ return self._entries.get(trace_id)
34
+
35
+ def list(self, limit: Optional[int] = None) -> List[DecisionLogEntry]:
36
+ with self._lock:
37
+ ids = self._order if limit is None else self._order[-limit:]
38
+ return [self._entries[t] for t in ids]
39
+
40
+ def attach_feedback(self, trace_id: str, feedback, outcome: str) -> Optional[DecisionLogEntry]:
41
+ with self._lock:
42
+ if trace_id not in self._entries:
43
+ return None
44
+ entry = self._entries[trace_id]
45
+ entry.outcome = outcome
46
+ entry.feedback = feedback.to_dict() if hasattr(feedback, "to_dict") else feedback
47
+ # 反馈使日志条目定版:重算该条及其后续条目的哈希链
48
+ self._rehash_from(self._order.index(trace_id))
49
+ return entry
50
+
51
+ def _rehash_from(self, start: int) -> None:
52
+ prev = self._entries[self._order[start - 1]].entry_hash if start > 0 else None
53
+ for i in range(start, len(self._order)):
54
+ entry = self._entries[self._order[i]]
55
+ entry.prev_hash = prev
56
+ entry.entry_hash = self._hash(entry)
57
+ prev = entry.entry_hash
58
+ self._prev_hash = prev
59
+
60
+ def verify_chain(self) -> bool:
61
+ with self._lock:
62
+ prev: Optional[str] = None
63
+ for trace_id in self._order:
64
+ entry = self._entries[trace_id]
65
+ if entry.prev_hash != prev:
66
+ return False
67
+ if entry.entry_hash != self._hash(entry):
68
+ return False
69
+ prev = entry.entry_hash
70
+ return True
71
+
72
+ @staticmethod
73
+ def _hash(entry: DecisionLogEntry) -> str:
74
+ body = {
75
+ "trace_id": entry.trace_id,
76
+ "request": entry.request,
77
+ "response": entry.response,
78
+ "node_version": entry.node_version,
79
+ "outcome": entry.outcome,
80
+ "feedback": entry.feedback,
81
+ "timestamp": entry.timestamp,
82
+ "prev_hash": entry.prev_hash,
83
+ }
84
+ canonical = json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
85
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
86
+
87
+ def save_jsonl(self, path: str) -> None:
88
+ with self._lock, open(path, "w", encoding="utf-8") as f:
89
+ for trace_id in self._order:
90
+ f.write(json.dumps(self._entries[trace_id].to_dict(), ensure_ascii=False) + "\n")
firefly/bus/bus.py ADDED
@@ -0,0 +1,249 @@
1
+ """萤总线 FireflyBus(方案 5.3 / 6.2)。
2
+
3
+ 职责:节点注册与路由、短缓存、软超时与回退、决策审计(哈希链)、
4
+ 反馈驱动的在线学习、工具图谱变更、指标采集。
5
+ ``federate`` / ``bid`` 属于 v0.2 能力,在 P0 显式留口。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import logging
11
+ import threading
12
+ from typing import Any, Dict, Optional, Union
13
+
14
+ from ..graph.tool_graph import ToolGraph
15
+ from ..nodes.base import FireflyNode
16
+ from ..protocol.models import (
17
+ DecisionLogEntry,
18
+ DecisionRequest,
19
+ DecisionResponse,
20
+ Feedback,
21
+ GraphDelta,
22
+ ToolSpec,
23
+ )
24
+ from ..protocol.schemas import validate_message
25
+ from ..protocol.version import PROTOCOL_VERSION
26
+ from .audit import AuditLog
27
+ from .cache import DecisionCache
28
+ from .fallback import Fallback
29
+ from .metrics import Metrics
30
+
31
+ BASE_DECISION_COST = 1e-5
32
+ PER_OPTION_COST = 1e-6
33
+ LLM_FALLBACK_COST = 1e-3
34
+ logger = logging.getLogger("firefly.bus")
35
+
36
+
37
+ class FireflyError(Exception):
38
+ """萤总线错误基类。"""
39
+
40
+
41
+ class ProtocolViolation(FireflyError):
42
+ """消息违反协议 Schema。"""
43
+
44
+
45
+ class NodeNotFound(FireflyError):
46
+ """请求的萤节点未注册。"""
47
+
48
+
49
+ class FireflyBus:
50
+ def __init__(
51
+ self,
52
+ graph: Optional[ToolGraph] = None,
53
+ cache: Optional[DecisionCache] = None,
54
+ audit: Optional[AuditLog] = None,
55
+ metrics: Optional[Metrics] = None,
56
+ fallback: Optional[Fallback] = None,
57
+ ) -> None:
58
+ self.graph = graph or ToolGraph()
59
+ self.cache = cache or DecisionCache()
60
+ self.audit = audit or AuditLog()
61
+ self.metrics = metrics or Metrics()
62
+ self.fallback = fallback or Fallback()
63
+ self._nodes: Dict[str, FireflyNode] = {}
64
+ self._decision_listeners = []
65
+ self._feedback_listeners = []
66
+ self._lock = threading.RLock()
67
+
68
+ def add_decision_listener(self, listener) -> None:
69
+ """listener(DecisionRequest, DecisionResponse),异常被隔离不影响决策。"""
70
+ self._decision_listeners.append(listener)
71
+
72
+ def add_feedback_listener(self, listener) -> None:
73
+ """listener(DecisionRequest, DecisionResponse, Feedback)。"""
74
+ self._feedback_listeners.append(listener)
75
+
76
+ # ------------------------------------------------------------------ #
77
+ # 节点与图谱
78
+ # ------------------------------------------------------------------ #
79
+ def register_node(self, node: FireflyNode, name: Optional[str] = None) -> FireflyNode:
80
+ with self._lock:
81
+ if node.graph is None:
82
+ node.graph = self.graph
83
+ key = name or node.node_type
84
+ self._nodes[key] = node
85
+ return node
86
+
87
+ def get_node(self, name: str) -> Optional[FireflyNode]:
88
+ return self._nodes.get(name)
89
+
90
+ def list_nodes(self) -> List[str]:
91
+ with self._lock:
92
+ return list(self._nodes.keys())
93
+
94
+ def register_tool(self, spec: Union[ToolSpec, Dict[str, Any]]) -> ToolSpec:
95
+ spec = spec if isinstance(spec, ToolSpec) else ToolSpec.from_dict(spec)
96
+ return self.graph.upsert_tool(spec)
97
+
98
+ def update_graph(self, delta: Union[GraphDelta, Dict[str, Any]]) -> None:
99
+ delta = delta if isinstance(delta, GraphDelta) else GraphDelta.from_dict(delta)
100
+ self.graph.apply_delta(delta)
101
+
102
+ # ------------------------------------------------------------------ #
103
+ # 决策
104
+ # ------------------------------------------------------------------ #
105
+ def decide(self, request: Union[DecisionRequest, Dict[str, Any]]) -> DecisionResponse:
106
+ if isinstance(request, dict):
107
+ # 裸边车客户端可省略协议版本,按当前版本补全后再做 schema 校验
108
+ payload = {"protocol_version": PROTOCOL_VERSION, **request}
109
+ errors = validate_message("decision_request", payload)
110
+ if errors:
111
+ raise ProtocolViolation("DecisionRequest 校验失败: " + "; ".join(errors))
112
+ request = DecisionRequest.from_dict(payload)
113
+ else:
114
+ errors = validate_message("decision_request", request.to_dict())
115
+ if errors:
116
+ raise ProtocolViolation("DecisionRequest 校验失败: " + "; ".join(errors))
117
+
118
+ node = self._nodes.get(request.node)
119
+ if node is None:
120
+ raise NodeNotFound(f"未注册的萤节点: {request.node}")
121
+
122
+ cache_key = self._cache_key(request)
123
+ cached = self.cache.get(cache_key)
124
+ if cached is not None:
125
+ response = copy.deepcopy(cached)
126
+ response.trace_id = request.trace_id
127
+ response.latency_ms = 0.0
128
+ response.extra["cache_hit"] = True
129
+ cache_hit = True
130
+ else:
131
+ response = node.decide(request)
132
+ cache_hit = False
133
+ self._apply_fallback_and_limits(request, response, node)
134
+ response_errors = validate_message("decision_response", response.to_dict())
135
+ if response_errors:
136
+ raise ProtocolViolation("DecisionResponse 校验失败: " + "; ".join(response_errors))
137
+ if not response.fallback_used:
138
+ self.cache.set(cache_key, response)
139
+
140
+ self.audit.append(
141
+ DecisionLogEntry(
142
+ trace_id=request.trace_id,
143
+ request=request.to_dict(),
144
+ response=response.to_dict(),
145
+ node_version=response.version,
146
+ )
147
+ )
148
+ self.metrics.record_decision(
149
+ request.node, response.latency_ms, response.fallback_used, cache_hit
150
+ )
151
+ for listener in list(self._decision_listeners):
152
+ try:
153
+ listener(request, response)
154
+ except Exception: # noqa: BLE001
155
+ logger.exception("决策监听器执行异常")
156
+ return response
157
+
158
+ def feedback(
159
+ self,
160
+ trace_id: str,
161
+ feedback: Union[Feedback, Dict[str, Any], str],
162
+ corrected_choice: Any = None,
163
+ extra: Optional[Dict[str, Any]] = None,
164
+ ) -> None:
165
+ """结果回流:触发节点在线学习并闭环审计日志(方案 7.4)。"""
166
+ if isinstance(feedback, str):
167
+ feedback = Feedback(
168
+ outcome=feedback,
169
+ corrected_choice=corrected_choice,
170
+ extra=extra or {},
171
+ )
172
+ elif not isinstance(feedback, Feedback):
173
+ feedback = Feedback.from_dict(feedback)
174
+ entry = self.audit.get(trace_id)
175
+ if entry is None:
176
+ raise KeyError(f"找不到 trace_id 对应的决策日志: {trace_id}")
177
+
178
+ request = DecisionRequest.from_dict(entry.request)
179
+ response = DecisionResponse.from_dict(entry.response)
180
+ node = self._nodes.get(response.node)
181
+ if node is not None:
182
+ node.handle_feedback(request, response, feedback)
183
+
184
+ self.audit.attach_feedback(trace_id, feedback, feedback.outcome)
185
+ self.metrics.record_feedback(response.node, feedback.outcome, response.confidence)
186
+ self.cache.clear() # 学习发生后旧缓存失效
187
+ for listener in list(self._feedback_listeners):
188
+ try:
189
+ listener(request, response, feedback)
190
+ except Exception: # noqa: BLE001
191
+ logger.exception("反馈监听器执行异常")
192
+
193
+ # ------------------------------------------------------------------ #
194
+ # v0.2 能力留口
195
+ # ------------------------------------------------------------------ #
196
+ def federate(self, request: Any) -> Any:
197
+ raise NotImplementedError("联邦萤群为 v0.2(L3)能力,P0 暂未启用")
198
+
199
+ def bid(self, task: Any) -> Any:
200
+ raise NotImplementedError("判断市场为 v0.2(L4)能力,P0 暂未启用")
201
+
202
+ # ------------------------------------------------------------------ #
203
+ # 内部
204
+ # ------------------------------------------------------------------ #
205
+ def _apply_fallback_and_limits(
206
+ self, request: DecisionRequest, response: DecisionResponse, node: FireflyNode
207
+ ) -> None:
208
+ policy = request.constraints.fallback
209
+ timed_out = response.latency_ms > request.constraints.timeout_ms
210
+
211
+ decision = None
212
+ if timed_out and policy and policy.mode != "none":
213
+ decision = self.fallback.dispatch(policy.mode, request, response, node)
214
+ response.rationale = (response.rationale or "") + ";决策超过超时预算,触发强制回退"
215
+ else:
216
+ decision = self.fallback.resolve(request, response, node)
217
+
218
+ if decision is not None and decision.mode != "none":
219
+ response.choice = decision.choice
220
+ response.fallback_used = True
221
+ response.fallback_mode = decision.mode
222
+ response.rationale = f"回退决策({decision.mode})" + (
223
+ f":{response.rationale}" if response.rationale else ""
224
+ )
225
+
226
+ mode = response.fallback_mode
227
+ if mode == "llm":
228
+ response.cost = LLM_FALLBACK_COST
229
+ else:
230
+ response.cost = round(
231
+ BASE_DECISION_COST + PER_OPTION_COST * len(request.options), 8
232
+ )
233
+
234
+ @staticmethod
235
+ def _as_request(request: Union[DecisionRequest, Dict[str, Any]]) -> DecisionRequest:
236
+ if isinstance(request, DecisionRequest):
237
+ return request
238
+ return DecisionRequest.from_dict(request)
239
+
240
+ @staticmethod
241
+ def _cache_key(request: DecisionRequest) -> str:
242
+ payload = {
243
+ "state": request.state.to_dict(),
244
+ "options": [o.to_dict() for o in request.options],
245
+ "constraints": request.constraints.to_dict(),
246
+ "explain": request.explain,
247
+ "counterfactual": request.counterfactual,
248
+ }
249
+ return DecisionCache.make_key(request.node, payload)
firefly/bus/cache.py ADDED
@@ -0,0 +1,55 @@
1
+ """决策短 TTL 缓存(方案 5.3:请求路由、缓存、批处理)。
2
+
3
+ 决策会随在线学习快速变化,因此缓存只用于极短时间内的相同请求去重。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import threading
10
+ import time
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+
14
+ class DecisionCache:
15
+ def __init__(self, ttl_seconds: float = 5.0, maxsize: int = 1024) -> None:
16
+ self.ttl_seconds = ttl_seconds
17
+ self.maxsize = maxsize
18
+ self._store: Dict[str, Tuple[float, Any]] = {}
19
+ self._lock = threading.Lock()
20
+
21
+ @staticmethod
22
+ def make_key(node: str, payload: Dict[str, Any]) -> str:
23
+ canonical = json.dumps(
24
+ {"node": node, "payload": payload},
25
+ ensure_ascii=False, sort_keys=True, separators=(",", ":"),
26
+ default=str,
27
+ )
28
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
29
+
30
+ def get(self, key: str) -> Optional[Any]:
31
+ if self.ttl_seconds <= 0:
32
+ return None
33
+ with self._lock:
34
+ item = self._store.get(key)
35
+ if item is None:
36
+ return None
37
+ expires_at, value = item
38
+ if time.time() > expires_at:
39
+ self._store.pop(key, None)
40
+ return None
41
+ return value
42
+
43
+ def set(self, key: str, value: Any) -> None:
44
+ if self.ttl_seconds <= 0:
45
+ return
46
+ with self._lock:
47
+ if len(self._store) >= self.maxsize:
48
+ # 简单淘汰最早写入的一个键
49
+ oldest = min(self._store.items(), key=lambda kv: kv[1][0])[0]
50
+ self._store.pop(oldest, None)
51
+ self._store[key] = (time.time() + self.ttl_seconds, value)
52
+
53
+ def clear(self) -> None:
54
+ with self._lock:
55
+ self._store.clear()
@@ -0,0 +1,106 @@
1
+ """回退器 Fallback(方案 6.4)。
2
+
3
+ 触发规则:
4
+ 1. 萤置信度低于阈值 → 回退到约束指定模式(llm / user / explore / deny);
5
+ 2. 高风险任务且置信度不足 → 优先人类;
6
+ 3. 元认知检测到“状态信息不足” → 回退;
7
+ 4. 守门硬规则裁决(带 certificate,如 block)永不回退 —— 安全不能被 LLM 覆盖。
8
+
9
+ LLM/用户的判断结果在总线收到反馈后作为教师信号回流训练(方案 6.4/7.4)。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Any, Callable, Optional
15
+
16
+ from ..nodes.base import FireflyNode
17
+ from ..protocol.models import DecisionRequest, DecisionResponse
18
+
19
+ # (request, firefly_response) -> 选项 id
20
+ Decider = Callable[[DecisionRequest, DecisionResponse], Any]
21
+
22
+
23
+ @dataclass
24
+ class FallbackDecision:
25
+ mode: str
26
+ choice: Any = None
27
+
28
+
29
+ class Fallback:
30
+ def __init__(
31
+ self,
32
+ llm_decider: Optional[Decider] = None,
33
+ user_decider: Optional[Decider] = None,
34
+ high_risk_threshold: float = 0.8,
35
+ ) -> None:
36
+ self.llm_decider = llm_decider
37
+ self.user_decider = user_decider
38
+ self.high_risk_threshold = high_risk_threshold
39
+
40
+ def resolve(
41
+ self,
42
+ request: DecisionRequest,
43
+ response: DecisionResponse,
44
+ node: FireflyNode,
45
+ ) -> Optional[FallbackDecision]:
46
+ policy = request.constraints.fallback
47
+ mode = policy.mode if policy else "llm"
48
+ threshold = policy.threshold if policy else 0.6
49
+ if mode == "none":
50
+ return None
51
+
52
+ # 安全硬规则裁决不可回退(方案 9.1)
53
+ if response.certificate:
54
+ return None
55
+
56
+ trigger: Optional[str] = None
57
+ if response.confidence < threshold:
58
+ trigger = mode
59
+ elif request.state.risk == "high" and response.confidence < self.high_risk_threshold:
60
+ trigger = "user"
61
+ elif response.meta_cognition and any(
62
+ "状态信息不足" in unknown for unknown in response.meta_cognition.known_unknowns
63
+ ):
64
+ trigger = mode
65
+ if trigger is None:
66
+ return None
67
+
68
+ return self.dispatch(trigger, request, response, node)
69
+
70
+ def dispatch(
71
+ self,
72
+ mode: str,
73
+ request: DecisionRequest,
74
+ response: DecisionResponse,
75
+ node: FireflyNode,
76
+ ) -> FallbackDecision:
77
+ """按指定模式强制执行一次回退(用于决策超时等场景)。"""
78
+ if mode == "none":
79
+ return FallbackDecision("none", response.choice)
80
+ return self._dispatch(mode, request, response, node)
81
+
82
+ def _dispatch(
83
+ self,
84
+ mode: str,
85
+ request: DecisionRequest,
86
+ response: DecisionResponse,
87
+ node: FireflyNode,
88
+ ) -> FallbackDecision:
89
+ if mode == "llm":
90
+ if self.llm_decider is not None:
91
+ return FallbackDecision("llm", self.llm_decider(request, response))
92
+ mode = "explore" # 未配置 LLM 时降级为探索
93
+ if mode == "user":
94
+ if self.user_decider is not None:
95
+ return FallbackDecision("user", self.user_decider(request, response))
96
+ if self.llm_decider is not None:
97
+ return FallbackDecision("llm", self.llm_decider(request, response))
98
+ mode = "deny"
99
+ if mode == "explore":
100
+ # 使用节点自身的决策空间(如过滤/守门的固定动作集)
101
+ return FallbackDecision("explore", node.explore_option(
102
+ request, node.prepare_options(request)
103
+ ))
104
+ if mode == "deny":
105
+ return FallbackDecision("deny", None)
106
+ return FallbackDecision(mode, None)
firefly/bus/metrics.py ADDED
@@ -0,0 +1,67 @@
1
+ """可观测指标(方案 9.3)。
2
+
3
+ 记录 decision_count / latency_p50,p95 / fallback_rate / cache_hit /
4
+ feedback_outcome 等,按节点维度聚合。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import threading
9
+ from collections import defaultdict
10
+ from typing import Dict, List
11
+
12
+
13
+ class Metrics:
14
+ def __init__(self) -> None:
15
+ self._lock = threading.Lock()
16
+ self._counters: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
17
+ self._latencies: Dict[str, List[float]] = defaultdict(list)
18
+ self._confidences: Dict[str, List[int]] = defaultdict(list) # (confidence*100, outcome)
19
+
20
+ def record_decision(
21
+ self, node: str, latency_ms: float, fallback_used: bool, cache_hit: bool
22
+ ) -> None:
23
+ with self._lock:
24
+ c = self._counters[node]
25
+ c["decision_count"] += 1
26
+ if fallback_used:
27
+ c["fallback_count"] += 1
28
+ if cache_hit:
29
+ c["cache_hit_count"] += 1
30
+ self._latencies[node].append(latency_ms)
31
+
32
+ def record_feedback(self, node: str, outcome: str, confidence: float) -> None:
33
+ with self._lock:
34
+ self._counters[node][f"feedback_{outcome}"] += 1
35
+ self._confidences[node].append(
36
+ (round(confidence, 2), 1 if outcome == "success" else 0)
37
+ )
38
+ self._confidences[node] = self._confidences[node][-500:]
39
+
40
+ def snapshot(self) -> Dict[str, Dict[str, object]]:
41
+ with self._lock:
42
+ result: Dict[str, Dict[str, object]] = {}
43
+ for node, counters in self._counters.items():
44
+ count = counters.get("decision_count", 0)
45
+ latencies = sorted(self._latencies.get(node, []))
46
+ result[node] = {
47
+ "decision_count": count,
48
+ "fallback_count": counters.get("fallback_count", 0),
49
+ "fallback_rate": round(
50
+ counters.get("fallback_count", 0) / count, 4
51
+ ) if count else 0.0,
52
+ "cache_hit_count": counters.get("cache_hit_count", 0),
53
+ "latency_p50_ms": _percentile(latencies, 50),
54
+ "latency_p95_ms": _percentile(latencies, 95),
55
+ "feedback": {
56
+ k.replace("feedback_", ""): v
57
+ for k, v in counters.items() if k.startswith("feedback_")
58
+ },
59
+ }
60
+ return result
61
+
62
+
63
+ def _percentile(sorted_values: List[float], pct: int) -> float:
64
+ if not sorted_values:
65
+ return 0.0
66
+ index = min(len(sorted_values) - 1, int(len(sorted_values) * pct / 100))
67
+ return round(sorted_values[index], 3)
@@ -0,0 +1,13 @@
1
+ """萤核层:编码、打分、校准、因果/元认知头。"""
2
+ from .calibrator import TemperatureCalibrator, expected_calibration_error
3
+ from .encoder import OptionEncoder, StateEncoder, l2_normalize, tokenize
4
+ from .firefly_core import FireflyCore
5
+ from .heads import CounterfactualHead, MetaCognitionHead
6
+ from .scorer import BilinearScorer, ScoredResult, softmax
7
+
8
+ __all__ = [
9
+ "FireflyCore", "BilinearScorer", "ScoredResult", "softmax",
10
+ "StateEncoder", "OptionEncoder", "tokenize", "l2_normalize",
11
+ "TemperatureCalibrator", "expected_calibration_error",
12
+ "CounterfactualHead", "MetaCognitionHead",
13
+ ]