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.
- firefly/__init__.py +6 -0
- firefly/bootstrap.py +38 -0
- firefly/bus/__init__.py +17 -0
- firefly/bus/audit.py +90 -0
- firefly/bus/bus.py +249 -0
- firefly/bus/cache.py +55 -0
- firefly/bus/fallback.py +106 -0
- firefly/bus/metrics.py +67 -0
- firefly/core/__init__.py +13 -0
- firefly/core/calibrator.py +77 -0
- firefly/core/encoder.py +126 -0
- firefly/core/firefly_core.py +151 -0
- firefly/core/heads.py +96 -0
- firefly/core/scorer.py +192 -0
- firefly/evolution/__init__.py +54 -0
- firefly/evolution/ab.py +130 -0
- firefly/evolution/change_detector.py +227 -0
- firefly/evolution/data_synthesizer.py +202 -0
- firefly/evolution/discovery.py +247 -0
- firefly/evolution/engine.py +496 -0
- firefly/evolution/forgetter.py +51 -0
- firefly/evolution/publisher.py +273 -0
- firefly/evolution/replay.py +87 -0
- firefly/evolution/samples.py +58 -0
- firefly/evolution/shadow.py +111 -0
- firefly/evolution/trainer.py +112 -0
- firefly/graph/__init__.py +4 -0
- firefly/graph/tool_graph.py +187 -0
- firefly/nodes/__init__.py +16 -0
- firefly/nodes/base.py +143 -0
- firefly/nodes/gate.py +207 -0
- firefly/nodes/loop.py +115 -0
- firefly/nodes/memory.py +136 -0
- firefly/nodes/meta.py +119 -0
- firefly/nodes/router.py +64 -0
- firefly/nodes/schedule.py +76 -0
- firefly/nodes/verify.py +99 -0
- firefly/protocol/__init__.py +38 -0
- firefly/protocol/models.py +469 -0
- firefly/protocol/schemas/decision_log.schema.json +24 -0
- firefly/protocol/schemas/decision_request.schema.json +100 -0
- firefly/protocol/schemas/decision_response.schema.json +74 -0
- firefly/protocol/schemas/tool_spec.schema.json +42 -0
- firefly/protocol/schemas.py +27 -0
- firefly/protocol/validation.py +89 -0
- firefly/protocol/version.py +5 -0
- firefly/sdk/__init__.py +4 -0
- firefly/sdk/client.py +160 -0
- firefly/server/__init__.py +4 -0
- firefly/server/http.py +124 -0
- firefly_judgment-0.1.0.dist-info/METADATA +276 -0
- firefly_judgment-0.1.0.dist-info/RECORD +56 -0
- firefly_judgment-0.1.0.dist-info/WHEEL +5 -0
- firefly_judgment-0.1.0.dist-info/entry_points.txt +2 -0
- firefly_judgment-0.1.0.dist-info/licenses/LICENSE +21 -0
- firefly_judgment-0.1.0.dist-info/top_level.txt +1 -0
firefly/__init__.py
ADDED
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
|
firefly/bus/__init__.py
ADDED
|
@@ -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()
|
firefly/bus/fallback.py
ADDED
|
@@ -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)
|
firefly/core/__init__.py
ADDED
|
@@ -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
|
+
]
|