paigent 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.
- pai/__init__.py +37 -0
- pai/__main__.py +52 -0
- pai/actions.py +47 -0
- pai/brain.py +137 -0
- pai/core.py +202 -0
- pai/finetune.py +290 -0
- pai/learning.py +142 -0
- pai/loader.py +194 -0
- pai/local_brain.py +106 -0
- pai/memory.py +116 -0
- pai/omni_brain.py +199 -0
- pai/paifile.py +338 -0
- pai/policy.py +92 -0
- pai/protocol.py +110 -0
- pai/py.typed +0 -0
- pai/server_brain.py +156 -0
- pai/triggers.py +111 -0
- paigent-0.1.0.dist-info/METADATA +210 -0
- paigent-0.1.0.dist-info/RECORD +23 -0
- paigent-0.1.0.dist-info/WHEEL +5 -0
- paigent-0.1.0.dist-info/entry_points.txt +2 -0
- paigent-0.1.0.dist-info/licenses/LICENSE +21 -0
- paigent-0.1.0.dist-info/top_level.txt +1 -0
pai/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""PAI — Proactive AI Framework (主動式 AI 框架)
|
|
2
|
+
|
|
3
|
+
感知 (Triggers) → 決策 (Brain) → 治理 (Policy) → 行動 (Actions) → 記憶/回饋 (Memory)
|
|
4
|
+
"""
|
|
5
|
+
from .core import PAIAgent, Event, Intent, AutonomyLevel
|
|
6
|
+
from .triggers import IntervalTrigger, ScheduleTrigger, FileWatchTrigger, ThresholdTrigger
|
|
7
|
+
from .brain import RuleBrain, LLMBrain, Rule
|
|
8
|
+
from .policy import ProactivityPolicy
|
|
9
|
+
from .actions import ConsoleNotifier, WebhookNotifier, CallbackAction
|
|
10
|
+
from .memory import Memory
|
|
11
|
+
from .protocol import PAI_PROTOCOL_VERSION, build_record, to_json, save_pai, load_pai
|
|
12
|
+
from .paifile import PaiWriter, PaiReader, pack_agent, load_agent, bake_adapter_into_pai
|
|
13
|
+
from .finetune import (
|
|
14
|
+
AdapterStore, EvalGate, SelfFinetuneManager,
|
|
15
|
+
EchoBackend, LlamaFinetuneBackend, LlamaFactoryBackend, export_preference_dataset,
|
|
16
|
+
)
|
|
17
|
+
from .omni_brain import MiniCPMoBrain, DuplexOmniLoop
|
|
18
|
+
from .loader import load_runtime
|
|
19
|
+
from .learning import ReflectiveMemory, HashingEmbedder, EmbeddingClient
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
__author__ = "vito1317 <service@vito1317.com>"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"PAIAgent", "Event", "Intent", "AutonomyLevel",
|
|
26
|
+
"IntervalTrigger", "ScheduleTrigger", "FileWatchTrigger", "ThresholdTrigger",
|
|
27
|
+
"RuleBrain", "LLMBrain", "Rule",
|
|
28
|
+
"ProactivityPolicy",
|
|
29
|
+
"ConsoleNotifier", "WebhookNotifier", "CallbackAction",
|
|
30
|
+
"Memory",
|
|
31
|
+
"PAI_PROTOCOL_VERSION", "build_record", "to_json", "save_pai", "load_pai",
|
|
32
|
+
"PaiWriter", "PaiReader", "pack_agent", "load_agent", "load_runtime",
|
|
33
|
+
"ReflectiveMemory", "HashingEmbedder", "EmbeddingClient",
|
|
34
|
+
"bake_adapter_into_pai", "AdapterStore", "EvalGate", "SelfFinetuneManager",
|
|
35
|
+
"EchoBackend", "LlamaFinetuneBackend", "LlamaFactoryBackend", "export_preference_dataset",
|
|
36
|
+
"MiniCPMoBrain", "DuplexOmniLoop",
|
|
37
|
+
]
|
pai/__main__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""PAI CLI:
|
|
2
|
+
python3 -m pai info <agent.pai> 顯示 .pai 檔內容
|
|
3
|
+
python3 -m pai run <agent.pai> [秒數] 載入並運行 agent
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
if len(sys.argv) < 3 or sys.argv[1] not in ("info", "run"):
|
|
12
|
+
print(__doc__)
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
cmd, path = sys.argv[1], sys.argv[2]
|
|
15
|
+
|
|
16
|
+
if cmd == "info":
|
|
17
|
+
from .paifile import PaiReader, load_agent
|
|
18
|
+
r = PaiReader(path)
|
|
19
|
+
print(json.dumps({**load_agent(path)["manifest"], **r.info()},
|
|
20
|
+
ensure_ascii=False, indent=2))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
logging.basicConfig(level=logging.INFO,
|
|
24
|
+
format="%(asctime)s %(levelname)s %(message)s")
|
|
25
|
+
duration = float(sys.argv[3]) if len(sys.argv) > 3 else None
|
|
26
|
+
from .loader import load_runtime
|
|
27
|
+
|
|
28
|
+
def confirm(intent):
|
|
29
|
+
ans = input(f"\n❓ [PAI 請求確認] {intent.rationale} (y/N): ").strip().lower()
|
|
30
|
+
return ans == "y"
|
|
31
|
+
|
|
32
|
+
# CLI 模式下未注入的 handler/metric 一律以 stub 代替(不執行真動作)
|
|
33
|
+
class _StubHandlers(dict):
|
|
34
|
+
def get(self, key, default=None):
|
|
35
|
+
if key not in self:
|
|
36
|
+
self[key] = lambda intent, _k=key: print(
|
|
37
|
+
f" ⚙️ [stub] handler '{_k}' 未注入,僅示意執行:{intent.params}")
|
|
38
|
+
return self[key]
|
|
39
|
+
|
|
40
|
+
class _StubMetrics(dict):
|
|
41
|
+
def get(self, key, default=None):
|
|
42
|
+
if key not in self:
|
|
43
|
+
self[key] = lambda: 0.0
|
|
44
|
+
return self[key]
|
|
45
|
+
|
|
46
|
+
agent = load_runtime(path, handlers=_StubHandlers(),
|
|
47
|
+
metrics=_StubMetrics(), confirm_handler=confirm)
|
|
48
|
+
agent.run(duration=duration)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
pai/actions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""行動層:可被 Intent 觸發的動作。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from .core import Intent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConsoleNotifier:
|
|
11
|
+
"""把通知印到終端(開發/示範用)。"""
|
|
12
|
+
|
|
13
|
+
def execute(self, intent: Intent):
|
|
14
|
+
params = intent.params
|
|
15
|
+
title = params.get("title", intent.action)
|
|
16
|
+
body = params.get("body", intent.rationale)
|
|
17
|
+
print(f"\n🔔 [PAI 通知] {title}\n {body}")
|
|
18
|
+
return {"notified": True}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WebhookNotifier:
|
|
22
|
+
"""POST JSON 到任意 webhook(Slack/Discord/自建服務)。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, url: str):
|
|
25
|
+
self.url = url
|
|
26
|
+
|
|
27
|
+
def execute(self, intent: Intent):
|
|
28
|
+
import urllib.request
|
|
29
|
+
data = json.dumps({
|
|
30
|
+
"title": intent.params.get("title", intent.action),
|
|
31
|
+
"body": intent.params.get("body", intent.rationale),
|
|
32
|
+
"intent": intent.to_dict(),
|
|
33
|
+
}, ensure_ascii=False).encode()
|
|
34
|
+
req = urllib.request.Request(
|
|
35
|
+
self.url, data=data, headers={"content-type": "application/json"})
|
|
36
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
37
|
+
return {"status": resp.status}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CallbackAction:
|
|
41
|
+
"""把任意 Python 函式包成動作。"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, fn: Callable[[Intent], object]):
|
|
44
|
+
self.fn = fn
|
|
45
|
+
|
|
46
|
+
def execute(self, intent: Intent):
|
|
47
|
+
return self.fn(intent)
|
pai/brain.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""決策層:把 Event + 記憶上下文 轉成 0..n 個 Intent。
|
|
2
|
+
|
|
3
|
+
兩種腦:
|
|
4
|
+
- RuleBrain:純規則,零依賴、可離線運行,適合確定性場景與 LLM 的安全後備。
|
|
5
|
+
- LLMBrain:呼叫 LLM(預設 Anthropic API)做開放式判斷,輸出結構化 JSON 意圖。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Callable, Optional
|
|
14
|
+
|
|
15
|
+
from .core import AutonomyLevel, Event, Intent
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("pai.brain")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Rule:
|
|
22
|
+
"""單條規則:match(event, context) → 可選 Intent。"""
|
|
23
|
+
name: str
|
|
24
|
+
match: Callable[[Event, dict], Optional[Intent]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RuleBrain:
|
|
28
|
+
def __init__(self, rules: Optional[list[Rule]] = None):
|
|
29
|
+
self.rules = rules or []
|
|
30
|
+
|
|
31
|
+
def add_rule(self, rule: Rule) -> "RuleBrain":
|
|
32
|
+
self.rules.append(rule)
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def decide(self, event: Event, context: dict) -> list[Intent]:
|
|
36
|
+
intents = []
|
|
37
|
+
for rule in self.rules:
|
|
38
|
+
try:
|
|
39
|
+
intent = rule.match(event, context)
|
|
40
|
+
if intent is not None:
|
|
41
|
+
intents.append(intent)
|
|
42
|
+
except Exception: # noqa: BLE001
|
|
43
|
+
logger.exception("Rule '%s' raised", rule.name)
|
|
44
|
+
return intents
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_LLM_SYSTEM_PROMPT = """\
|
|
48
|
+
You are the decision engine of a proactive AI agent.
|
|
49
|
+
Given an event and context, decide whether the agent should act proactively.
|
|
50
|
+
|
|
51
|
+
Respond ONLY with a JSON array (possibly empty) of intents:
|
|
52
|
+
[{"action": "<one of the available actions>",
|
|
53
|
+
"params": {},
|
|
54
|
+
"confidence": 0.0-1.0,
|
|
55
|
+
"urgency": 0.0-1.0,
|
|
56
|
+
"rationale": "<short reason, same language as the user>",
|
|
57
|
+
"requested_level": 0|1|2|3}]
|
|
58
|
+
|
|
59
|
+
Levels: 0=observe only, 1=suggest/notify, 2=ask for confirmation, 3=act autonomously.
|
|
60
|
+
Be conservative: prefer lower levels unless confidence is high and risk is low.
|
|
61
|
+
Return [] when no proactive behavior is warranted (most events deserve []).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LLMBrain:
|
|
66
|
+
"""以 LLM 為決策核心。失敗時自動退回 fallback(通常是 RuleBrain)。"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, available_actions: list[str],
|
|
69
|
+
model: str = "claude-sonnet-4-6",
|
|
70
|
+
api_key: Optional[str] = None,
|
|
71
|
+
fallback: Optional[RuleBrain] = None,
|
|
72
|
+
user_profile: str = ""):
|
|
73
|
+
self.available_actions = available_actions
|
|
74
|
+
self.model = model
|
|
75
|
+
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
|
76
|
+
self.fallback = fallback
|
|
77
|
+
self.user_profile = user_profile
|
|
78
|
+
|
|
79
|
+
def decide(self, event: Event, context: dict) -> list[Intent]:
|
|
80
|
+
try:
|
|
81
|
+
return self._decide_llm(event, context)
|
|
82
|
+
except Exception: # noqa: BLE001
|
|
83
|
+
logger.exception("LLM decision failed; using fallback")
|
|
84
|
+
if self.fallback:
|
|
85
|
+
return self.fallback.decide(event, context)
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
def _decide_llm(self, event: Event, context: dict) -> list[Intent]:
|
|
89
|
+
import urllib.request
|
|
90
|
+
|
|
91
|
+
if not self.api_key:
|
|
92
|
+
raise RuntimeError("ANTHROPIC_API_KEY not set")
|
|
93
|
+
|
|
94
|
+
user_msg = json.dumps({
|
|
95
|
+
"event": event.to_dict(),
|
|
96
|
+
"context": context,
|
|
97
|
+
"available_actions": self.available_actions,
|
|
98
|
+
"user_profile": self.user_profile,
|
|
99
|
+
}, ensure_ascii=False)
|
|
100
|
+
|
|
101
|
+
req = urllib.request.Request(
|
|
102
|
+
"https://api.anthropic.com/v1/messages",
|
|
103
|
+
data=json.dumps({
|
|
104
|
+
"model": self.model,
|
|
105
|
+
"max_tokens": 1024,
|
|
106
|
+
"system": _LLM_SYSTEM_PROMPT,
|
|
107
|
+
"messages": [{"role": "user", "content": user_msg}],
|
|
108
|
+
}).encode(),
|
|
109
|
+
headers={
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
"x-api-key": self.api_key,
|
|
112
|
+
"anthropic-version": "2023-06-01",
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
116
|
+
data = json.loads(resp.read())
|
|
117
|
+
text = "".join(b.get("text", "") for b in data.get("content", []))
|
|
118
|
+
return self._parse(text)
|
|
119
|
+
|
|
120
|
+
def _parse(self, text: str) -> list[Intent]:
|
|
121
|
+
start, end = text.find("["), text.rfind("]")
|
|
122
|
+
if start == -1 or end == -1:
|
|
123
|
+
return []
|
|
124
|
+
items = json.loads(text[start:end + 1])
|
|
125
|
+
intents = []
|
|
126
|
+
for it in items:
|
|
127
|
+
if it.get("action") not in self.available_actions:
|
|
128
|
+
continue
|
|
129
|
+
intents.append(Intent(
|
|
130
|
+
action=it["action"],
|
|
131
|
+
params=it.get("params", {}),
|
|
132
|
+
confidence=float(it.get("confidence", 0.5)),
|
|
133
|
+
urgency=float(it.get("urgency", 0.5)),
|
|
134
|
+
rationale=it.get("rationale", ""),
|
|
135
|
+
requested_level=AutonomyLevel(int(it.get("requested_level", 1))),
|
|
136
|
+
))
|
|
137
|
+
return intents
|
pai/core.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""PAI 核心:事件模型、意圖模型、自主等級與主動行為迴圈。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
from typing import Any, Callable, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("pai")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AutonomyLevel(IntEnum):
|
|
17
|
+
"""自主等級:決定 PAI 對一個意圖「可以主動到什麼程度」。
|
|
18
|
+
|
|
19
|
+
OBSERVE: 只記錄,不打擾
|
|
20
|
+
SUGGEST: 主動通知/建議,但不執行
|
|
21
|
+
ASK: 請求使用者確認後執行
|
|
22
|
+
ACT: 直接自動執行(高信心、低風險時)
|
|
23
|
+
"""
|
|
24
|
+
OBSERVE = 0
|
|
25
|
+
SUGGEST = 1
|
|
26
|
+
ASK = 2
|
|
27
|
+
ACT = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Event:
|
|
32
|
+
"""感知層產生的標準事件格式。"""
|
|
33
|
+
source: str # 觸發器名稱
|
|
34
|
+
kind: str # 事件類型,如 "schedule.tick", "file.changed"
|
|
35
|
+
payload: dict = field(default_factory=dict)
|
|
36
|
+
ts: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
37
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"id": self.id, "source": self.source, "kind": self.kind,
|
|
42
|
+
"payload": self.payload, "ts": self.ts.isoformat(),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _event_summary(event: "Event") -> str:
|
|
47
|
+
"""把事件壓成一行文字,供記憶檢索使用。"""
|
|
48
|
+
payload = " ".join(f"{k}={v}" for k, v in event.payload.items())
|
|
49
|
+
return f"{event.kind} from {event.source}: {payload}".strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Intent:
|
|
54
|
+
"""決策層輸出的標準意圖格式。"""
|
|
55
|
+
action: str # 想執行的動作名稱
|
|
56
|
+
params: dict = field(default_factory=dict)
|
|
57
|
+
confidence: float = 0.5 # 0~1,決策信心
|
|
58
|
+
urgency: float = 0.5 # 0~1,緊急程度
|
|
59
|
+
rationale: str = "" # 決策理由(可解釋性)
|
|
60
|
+
requested_level: AutonomyLevel = AutonomyLevel.SUGGEST
|
|
61
|
+
event_id: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"action": self.action, "params": self.params,
|
|
66
|
+
"confidence": self.confidence, "urgency": self.urgency,
|
|
67
|
+
"rationale": self.rationale,
|
|
68
|
+
"requested_level": int(self.requested_level),
|
|
69
|
+
"event_id": self.event_id,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PAIAgent:
|
|
74
|
+
"""主動式 AI 代理:把觸發器、決策腦、治理政策、行動與記憶組裝成迴圈。"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, name: str, brain, policy, memory, actions: dict[str, Any],
|
|
77
|
+
confirm_handler: Optional[Callable[[Intent], bool]] = None,
|
|
78
|
+
reflective=None):
|
|
79
|
+
self.name = name
|
|
80
|
+
self.brain = brain
|
|
81
|
+
self.policy = policy
|
|
82
|
+
self.memory = memory
|
|
83
|
+
self.actions = actions # {action_name: Action 實例}
|
|
84
|
+
self.reflective = reflective # ReflectiveMemory(記憶式即時學習,選用)
|
|
85
|
+
self.triggers: list = []
|
|
86
|
+
self.confirm_handler = confirm_handler or (lambda intent: False)
|
|
87
|
+
self._stop = threading.Event()
|
|
88
|
+
|
|
89
|
+
# ---- 組裝 ----
|
|
90
|
+
def add_trigger(self, trigger) -> "PAIAgent":
|
|
91
|
+
self.triggers.append(trigger)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
# ---- 主迴圈 ----
|
|
95
|
+
def run(self, duration: Optional[float] = None, poll_interval: float = 0.5):
|
|
96
|
+
"""啟動主動行為迴圈。duration=None 表示持續運行。"""
|
|
97
|
+
logger.info("[%s] PAI agent started (%d triggers)", self.name, len(self.triggers))
|
|
98
|
+
start = time.time()
|
|
99
|
+
try:
|
|
100
|
+
while not self._stop.is_set():
|
|
101
|
+
for trigger in self.triggers:
|
|
102
|
+
for event in trigger.poll():
|
|
103
|
+
self._handle_event(event)
|
|
104
|
+
if duration is not None and time.time() - start >= duration:
|
|
105
|
+
break
|
|
106
|
+
time.sleep(poll_interval)
|
|
107
|
+
finally:
|
|
108
|
+
logger.info("[%s] PAI agent stopped", self.name)
|
|
109
|
+
|
|
110
|
+
def stop(self):
|
|
111
|
+
self._stop.set()
|
|
112
|
+
|
|
113
|
+
# ---- 單一事件的完整生命週期 ----
|
|
114
|
+
def _handle_event(self, event: Event):
|
|
115
|
+
self.memory.record_event(event)
|
|
116
|
+
context = self.memory.build_context(event)
|
|
117
|
+
|
|
118
|
+
# 記憶式即時學習:把相似情境的回饋教訓注入決策上下文
|
|
119
|
+
if self.reflective is not None:
|
|
120
|
+
lessons = self.reflective.lessons_for(_event_summary(event))
|
|
121
|
+
if lessons:
|
|
122
|
+
context["lessons_from_feedback"] = lessons
|
|
123
|
+
|
|
124
|
+
intents = self.brain.decide(event, context)
|
|
125
|
+
for intent in intents:
|
|
126
|
+
intent.event_id = event.id
|
|
127
|
+
self._dispatch(intent, event)
|
|
128
|
+
|
|
129
|
+
def _dispatch(self, intent: Intent, event: Event):
|
|
130
|
+
from .protocol import build_record
|
|
131
|
+
|
|
132
|
+
context = self.memory.build_context(event)
|
|
133
|
+
granted = self.policy.gate(intent, self.memory)
|
|
134
|
+
cost = self.policy.last_interruption_cost
|
|
135
|
+
self.memory.record_intent(intent, granted=int(granted))
|
|
136
|
+
|
|
137
|
+
execution = {"actions_taken": [], "status": "not_executed"}
|
|
138
|
+
feedback = "pending"
|
|
139
|
+
|
|
140
|
+
if granted == AutonomyLevel.OBSERVE:
|
|
141
|
+
logger.debug("OBSERVE only: %s (%s)", intent.action, intent.rationale)
|
|
142
|
+
execution["status"] = "observed"
|
|
143
|
+
|
|
144
|
+
elif granted == AutonomyLevel.SUGGEST:
|
|
145
|
+
notifier = self.actions.get("__notify__")
|
|
146
|
+
if notifier:
|
|
147
|
+
notifier.execute(Intent(
|
|
148
|
+
action="__notify__",
|
|
149
|
+
params={"title": f"建議:{intent.action}",
|
|
150
|
+
"body": intent.rationale, "intent": intent.to_dict()},
|
|
151
|
+
confidence=intent.confidence, urgency=intent.urgency,
|
|
152
|
+
))
|
|
153
|
+
self.memory.record_outcome(intent, status="suggested")
|
|
154
|
+
execution["status"] = "suggested"
|
|
155
|
+
|
|
156
|
+
else: # ASK 或 ACT
|
|
157
|
+
action = self.actions.get(intent.action)
|
|
158
|
+
if action is None:
|
|
159
|
+
logger.warning("No action registered for intent '%s'", intent.action)
|
|
160
|
+
execution["status"] = "no_action_registered"
|
|
161
|
+
else:
|
|
162
|
+
approved = True
|
|
163
|
+
if granted == AutonomyLevel.ASK:
|
|
164
|
+
approved = self.confirm_handler(intent)
|
|
165
|
+
if not approved:
|
|
166
|
+
self.memory.record_outcome(intent, status="declined")
|
|
167
|
+
self.memory.record_feedback(intent, positive=False)
|
|
168
|
+
execution["status"] = "declined"
|
|
169
|
+
feedback = "rejected"
|
|
170
|
+
self._learn(event, intent, "rejected")
|
|
171
|
+
if approved:
|
|
172
|
+
try:
|
|
173
|
+
result = action.execute(intent)
|
|
174
|
+
self.memory.record_outcome(intent, status="executed", result=result)
|
|
175
|
+
execution["actions_taken"].append(
|
|
176
|
+
{"tool": intent.action, "status": "ok", "result": str(result)})
|
|
177
|
+
execution["status"] = "executed"
|
|
178
|
+
logger.info("Executed '%s' (confidence=%.2f): %s",
|
|
179
|
+
intent.action, intent.confidence, intent.rationale)
|
|
180
|
+
except Exception as exc: # noqa: BLE001
|
|
181
|
+
self.memory.record_outcome(intent, status="failed", result=str(exc))
|
|
182
|
+
execution["status"] = "failed"
|
|
183
|
+
logger.exception("Action '%s' failed", intent.action)
|
|
184
|
+
|
|
185
|
+
# ACT 成功且使用者沒有否決 → 視為一次正向經驗
|
|
186
|
+
if execution["status"] == "executed" and granted == AutonomyLevel.ACT:
|
|
187
|
+
self._learn(event, intent, "accepted")
|
|
188
|
+
|
|
189
|
+
# 產生並保存 PAI Protocol 標準紀錄
|
|
190
|
+
record = build_record(event, context, intent, granted, cost,
|
|
191
|
+
execution=execution, user_feedback=feedback)
|
|
192
|
+
self.memory.record_protocol(record)
|
|
193
|
+
|
|
194
|
+
# ---- 記憶式學習:對外的回饋 API ----
|
|
195
|
+
def record_user_feedback(self, event: Event, intent: Intent, feedback: str):
|
|
196
|
+
"""外部 UI 收到使用者回饋(accepted/rejected/modified/ignored)時呼叫。"""
|
|
197
|
+
self._learn(event, intent, feedback)
|
|
198
|
+
|
|
199
|
+
def _learn(self, event: Event, intent: Intent, feedback: str):
|
|
200
|
+
if self.reflective is not None:
|
|
201
|
+
self.reflective.add_experience(
|
|
202
|
+
_event_summary(event), intent.action, intent.rationale, feedback)
|