loopentx 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.
- loopentx/__init__.py +31 -0
- loopentx/backends/__init__.py +11 -0
- loopentx/backends/base.py +115 -0
- loopentx/backends/memory.py +146 -0
- loopentx/backends/redis_backend.py +196 -0
- loopentx/cli/__init__.py +0 -0
- loopentx/cli/main.py +398 -0
- loopentx/core/__init__.py +28 -0
- loopentx/core/config.py +67 -0
- loopentx/core/context.py +362 -0
- loopentx/core/events.py +35 -0
- loopentx/core/exceptions.py +67 -0
- loopentx/core/loop.py +240 -0
- loopentx/core/memory.py +110 -0
- loopentx/core/models.py +172 -0
- loopentx/core/orchestrator.py +166 -0
- loopentx/core/skill.py +159 -0
- loopentx/llm/__init__.py +3 -0
- loopentx/llm/caller.py +103 -0
- loopentx/trust/__init__.py +4 -0
- loopentx/trust/policy.py +185 -0
- loopentx/trust/scorer.py +141 -0
- loopentx-0.1.0.dist-info/METADATA +555 -0
- loopentx-0.1.0.dist-info/RECORD +28 -0
- loopentx-0.1.0.dist-info/WHEEL +5 -0
- loopentx-0.1.0.dist-info/entry_points.txt +2 -0
- loopentx-0.1.0.dist-info/licenses/LICENSE +21 -0
- loopentx-0.1.0.dist-info/top_level.txt +1 -0
loopentx/llm/caller.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""LLM caller — routes ctx.think() to the configured provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from loopentx.core.config import get_config
|
|
11
|
+
|
|
12
|
+
log = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def call_llm(
|
|
16
|
+
prompt: str,
|
|
17
|
+
system: Optional[str] = None,
|
|
18
|
+
choose_from: Optional[list[str]] = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Call the configured LLM provider and return the response string.
|
|
21
|
+
|
|
22
|
+
Automatically routes to OpenAI or Anthropic based on config.
|
|
23
|
+
If choose_from is set, validates the response against the allowed values.
|
|
24
|
+
"""
|
|
25
|
+
cfg = get_config()
|
|
26
|
+
|
|
27
|
+
if cfg.llm_provider == "anthropic":
|
|
28
|
+
response = await _call_anthropic(prompt, system, cfg)
|
|
29
|
+
elif cfg.llm_provider == "openai":
|
|
30
|
+
response = await _call_openai(prompt, system, cfg)
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"Unsupported LLM provider: {cfg.llm_provider!r}")
|
|
33
|
+
|
|
34
|
+
response = response.strip()
|
|
35
|
+
|
|
36
|
+
if choose_from:
|
|
37
|
+
response = _extract_choice(response, choose_from)
|
|
38
|
+
|
|
39
|
+
log.info("llm.called", provider=cfg.llm_provider,
|
|
40
|
+
prompt_len=len(prompt), response_len=len(response))
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _call_anthropic(prompt: str, system: Optional[str], cfg: Any) -> str:
|
|
45
|
+
try:
|
|
46
|
+
import anthropic
|
|
47
|
+
except ImportError:
|
|
48
|
+
raise ImportError("pip install loopentx[anthropic]")
|
|
49
|
+
|
|
50
|
+
api_key = cfg.llm_api_key or None
|
|
51
|
+
client = anthropic.AsyncAnthropic(api_key=api_key)
|
|
52
|
+
|
|
53
|
+
kwargs: dict = dict(
|
|
54
|
+
model=cfg.llm_model,
|
|
55
|
+
max_tokens=1024,
|
|
56
|
+
messages=[{"role": "user", "content": prompt}],
|
|
57
|
+
)
|
|
58
|
+
if system:
|
|
59
|
+
kwargs["system"] = system
|
|
60
|
+
|
|
61
|
+
msg = await client.messages.create(**kwargs)
|
|
62
|
+
return msg.content[0].text
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _call_openai(prompt: str, system: Optional[str], cfg: Any) -> str:
|
|
66
|
+
try:
|
|
67
|
+
from openai import AsyncOpenAI
|
|
68
|
+
except ImportError:
|
|
69
|
+
raise ImportError("pip install loopentx[openai]")
|
|
70
|
+
|
|
71
|
+
api_key = cfg.llm_api_key or None
|
|
72
|
+
client = AsyncOpenAI(api_key=api_key)
|
|
73
|
+
|
|
74
|
+
messages = []
|
|
75
|
+
if system:
|
|
76
|
+
messages.append({"role": "system", "content": system})
|
|
77
|
+
messages.append({"role": "user", "content": prompt})
|
|
78
|
+
|
|
79
|
+
resp = await client.chat.completions.create(
|
|
80
|
+
model=cfg.llm_model,
|
|
81
|
+
messages=messages,
|
|
82
|
+
max_tokens=1024,
|
|
83
|
+
)
|
|
84
|
+
return resp.choices[0].message.content or ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _extract_choice(response: str, choices: list[str]) -> str:
|
|
88
|
+
"""Extract the matching choice from the LLM response."""
|
|
89
|
+
lower = response.lower().strip()
|
|
90
|
+
for choice in choices:
|
|
91
|
+
if choice.lower() in lower:
|
|
92
|
+
return choice
|
|
93
|
+
# Fuzzy: first word
|
|
94
|
+
first_word = re.split(r"\W+", lower)[0] if lower else ""
|
|
95
|
+
for choice in choices:
|
|
96
|
+
if choice.lower().startswith(first_word):
|
|
97
|
+
return choice
|
|
98
|
+
log.warning("llm.choice_not_found", response=response, choices=choices)
|
|
99
|
+
return choices[0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Allow Any import for type hints in private functions
|
|
103
|
+
from typing import Any
|
loopentx/trust/policy.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""The @policy decorator — capability scoping, shadow mode, blast radius."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from loopentx.core.models import BlastRadius, SkillRegistration, TrustLevel
|
|
12
|
+
from loopentx.core.exceptions import PolicyViolationError, SkillNotApprovedError
|
|
13
|
+
from loopentx.core.config import get_config
|
|
14
|
+
|
|
15
|
+
log = structlog.get_logger()
|
|
16
|
+
|
|
17
|
+
_WRITE_PATTERNS = [
|
|
18
|
+
"post_", "send_", "write_", "create_", "update_", "delete_",
|
|
19
|
+
"publish_", "notify_", "alert_", "push_", "emit_", "dispatch_",
|
|
20
|
+
"insert_", "patch_", "put_", "remove_", "drop_",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PolicyContext:
|
|
25
|
+
"""Runtime policy enforcement attached to a skill."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
skill_name: str,
|
|
30
|
+
can_read: list[str],
|
|
31
|
+
can_write: list[str],
|
|
32
|
+
blast_radius: BlastRadius,
|
|
33
|
+
shadow_cycles: int,
|
|
34
|
+
require_approval: bool,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.skill_name = skill_name
|
|
37
|
+
self.can_read = set(can_read)
|
|
38
|
+
self.can_write = set(can_write)
|
|
39
|
+
self.blast_radius = blast_radius
|
|
40
|
+
self.shadow_cycles = shadow_cycles
|
|
41
|
+
self.require_approval = require_approval
|
|
42
|
+
|
|
43
|
+
def assert_can_read(self, capability: str) -> None:
|
|
44
|
+
if capability not in self.can_read and capability not in self.can_write:
|
|
45
|
+
raise PolicyViolationError(self.skill_name, capability, "read")
|
|
46
|
+
|
|
47
|
+
def assert_can_write(self, capability: str) -> None:
|
|
48
|
+
if capability not in self.can_write:
|
|
49
|
+
raise PolicyViolationError(self.skill_name, capability, "write")
|
|
50
|
+
|
|
51
|
+
def is_write_action(self, fn: Callable) -> bool:
|
|
52
|
+
name = fn.__name__.lower()
|
|
53
|
+
return any(name.startswith(p) for p in _WRITE_PATTERNS)
|
|
54
|
+
|
|
55
|
+
def to_registration(self, fn_name: str) -> SkillRegistration:
|
|
56
|
+
needs_gate = self.shadow_cycles > 0 or self.require_approval
|
|
57
|
+
return SkillRegistration(
|
|
58
|
+
name=fn_name,
|
|
59
|
+
kind="skill",
|
|
60
|
+
can_read=list(self.can_read),
|
|
61
|
+
can_write=list(self.can_write),
|
|
62
|
+
blast_radius=self.blast_radius,
|
|
63
|
+
shadow_cycles=self.shadow_cycles,
|
|
64
|
+
shadow_cycles_remaining=self.shadow_cycles,
|
|
65
|
+
require_approval=self.require_approval,
|
|
66
|
+
is_active=not needs_gate,
|
|
67
|
+
is_shadow=self.shadow_cycles > 0,
|
|
68
|
+
trust_level=TrustLevel.UNTRUSTED if needs_gate else TrustLevel.PROVISIONAL,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def policy(
|
|
73
|
+
can_read: Optional[list[str]] = None,
|
|
74
|
+
can_write: Optional[list[str]] = None,
|
|
75
|
+
blast_radius: str = "low",
|
|
76
|
+
shadow_cycles: int = 0,
|
|
77
|
+
require_approval: bool = False,
|
|
78
|
+
) -> Callable:
|
|
79
|
+
"""Declare the trust policy for a skill.
|
|
80
|
+
|
|
81
|
+
Must be applied ABOVE @skill in the decorator stack.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
can_read: Systems the skill may read from.
|
|
85
|
+
can_write: Systems the skill may write to (implies read).
|
|
86
|
+
blast_radius: Impact scope: "low" | "medium" | "high" | "critical".
|
|
87
|
+
high/critical automatically require human approval.
|
|
88
|
+
shadow_cycles: Dry-run cycles before going live. Write actions
|
|
89
|
+
are captured but not applied during shadow mode.
|
|
90
|
+
require_approval: Require explicit `loopentx trust approve` before live.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
@policy(
|
|
94
|
+
can_read=["metrics_api"],
|
|
95
|
+
can_write=["slack"],
|
|
96
|
+
blast_radius="medium",
|
|
97
|
+
shadow_cycles=3,
|
|
98
|
+
)
|
|
99
|
+
@skill(retries=3)
|
|
100
|
+
async def triage(ctx, services: list[str]):
|
|
101
|
+
...
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
blast = BlastRadius(blast_radius)
|
|
105
|
+
except ValueError:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Invalid blast_radius: {blast_radius!r}. "
|
|
108
|
+
f"Must be one of: low, medium, high, critical"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
_require = require_approval or blast in (BlastRadius.HIGH, BlastRadius.CRITICAL)
|
|
112
|
+
|
|
113
|
+
def decorator(fn: Callable) -> Callable:
|
|
114
|
+
policy_ctx = PolicyContext(
|
|
115
|
+
skill_name=fn.__name__,
|
|
116
|
+
can_read=can_read or [],
|
|
117
|
+
can_write=can_write or [],
|
|
118
|
+
blast_radius=blast,
|
|
119
|
+
shadow_cycles=shadow_cycles,
|
|
120
|
+
require_approval=_require,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if hasattr(fn, "_loopentx_skill"):
|
|
124
|
+
fn._loopentx_skill.policy_context = policy_ctx
|
|
125
|
+
|
|
126
|
+
@functools.wraps(fn)
|
|
127
|
+
async def wrapper(**kwargs: Any) -> Any:
|
|
128
|
+
if shadow_cycles > 0 or _require:
|
|
129
|
+
cfg = get_config()
|
|
130
|
+
reg = await cfg.backend.get_skill_registration(fn.__name__)
|
|
131
|
+
if reg and not reg.is_active:
|
|
132
|
+
if reg.is_shadow and reg.shadow_cycles_remaining > 0:
|
|
133
|
+
return await _run_shadow(fn, policy_ctx, reg, **kwargs)
|
|
134
|
+
raise SkillNotApprovedError(fn.__name__)
|
|
135
|
+
return await fn(**kwargs)
|
|
136
|
+
|
|
137
|
+
wrapper._loopentx_policy = policy_ctx # type: ignore[attr-defined]
|
|
138
|
+
if hasattr(fn, "_loopentx_skill"):
|
|
139
|
+
wrapper._loopentx_skill = fn._loopentx_skill # type: ignore[attr-defined]
|
|
140
|
+
wrapper._loopentx_skill.policy_context = policy_ctx
|
|
141
|
+
wrapper._loopentx_kind = "skill" # type: ignore[attr-defined]
|
|
142
|
+
|
|
143
|
+
return wrapper
|
|
144
|
+
|
|
145
|
+
return decorator
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _run_shadow(
|
|
149
|
+
fn: Callable,
|
|
150
|
+
policy_ctx: PolicyContext,
|
|
151
|
+
reg: Any,
|
|
152
|
+
**kwargs: Any,
|
|
153
|
+
) -> Any:
|
|
154
|
+
from loopentx.core.context import LoopContext
|
|
155
|
+
from loopentx.core.config import get_config
|
|
156
|
+
from ulid import ULID
|
|
157
|
+
|
|
158
|
+
cfg = get_config()
|
|
159
|
+
run_id = str(ULID())
|
|
160
|
+
ctx = LoopContext(
|
|
161
|
+
run_id=run_id,
|
|
162
|
+
skill_name=fn.__name__,
|
|
163
|
+
backend=cfg.backend,
|
|
164
|
+
policy_context=policy_ctx,
|
|
165
|
+
shadow_mode=True,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if hasattr(fn, "_loopentx_skill"):
|
|
169
|
+
result = await fn._loopentx_skill.execute(ctx, **kwargs)
|
|
170
|
+
else:
|
|
171
|
+
result = await fn(ctx, **kwargs)
|
|
172
|
+
|
|
173
|
+
remaining = reg.shadow_cycles_remaining - 1
|
|
174
|
+
if remaining <= 0:
|
|
175
|
+
if policy_ctx.blast_radius == BlastRadius.LOW and not policy_ctx.require_approval:
|
|
176
|
+
await cfg.backend.approve_skill(fn.__name__, approved_by="auto")
|
|
177
|
+
log.info("policy.auto_approved", skill=fn.__name__)
|
|
178
|
+
else:
|
|
179
|
+
log.info("policy.shadow_complete_awaiting_approval",
|
|
180
|
+
skill=fn.__name__, blast=policy_ctx.blast_radius.value)
|
|
181
|
+
else:
|
|
182
|
+
reg.shadow_cycles_remaining = remaining
|
|
183
|
+
await cfg.backend.save_skill_registration(reg)
|
|
184
|
+
|
|
185
|
+
return result
|
loopentx/trust/scorer.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Trust scorer — evaluates skill reliability and promotes trust levels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from loopentx.core.models import TrustRecord, TrustLevel, RunStatus
|
|
11
|
+
from loopentx.core.config import get_config
|
|
12
|
+
|
|
13
|
+
log = structlog.get_logger()
|
|
14
|
+
|
|
15
|
+
THRESHOLDS = {
|
|
16
|
+
TrustLevel.AUTONOMOUS: 0.90,
|
|
17
|
+
TrustLevel.TRUSTED: 0.65,
|
|
18
|
+
TrustLevel.PROVISIONAL: 0.30,
|
|
19
|
+
TrustLevel.UNTRUSTED: 0.0,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
WEIGHTS = {
|
|
23
|
+
"success_rate": 0.45,
|
|
24
|
+
"human_approval_rate": 0.30,
|
|
25
|
+
"volume_bonus": 0.10,
|
|
26
|
+
"recency_factor": 0.15,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TrustScorer:
|
|
31
|
+
"""Calculates and updates trust scores for loops and skills.
|
|
32
|
+
|
|
33
|
+
Score components:
|
|
34
|
+
success_rate (45%) — ratio of completed to total live runs
|
|
35
|
+
human_approval_rate (30%)— ratio of approvals to human reviews
|
|
36
|
+
volume_bonus (10%) — logarithmic reward for proven track record
|
|
37
|
+
recency_factor (15%) — penalises dormant skills
|
|
38
|
+
|
|
39
|
+
Score → Level:
|
|
40
|
+
0.00–0.29 UNTRUSTED shadow + human review required
|
|
41
|
+
0.30–0.64 PROVISIONAL runs live, changes monitored
|
|
42
|
+
0.65–0.89 TRUSTED autonomous, changes need review
|
|
43
|
+
0.90–1.00 AUTONOMOUS fully autonomous, changes auto-approved
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
async def evaluate(self, skill_name: str) -> TrustRecord:
|
|
47
|
+
cfg = get_config()
|
|
48
|
+
since = time.time() - (30 * 86400)
|
|
49
|
+
runs = await cfg.backend.get_runs(skill_name=skill_name, since=since)
|
|
50
|
+
|
|
51
|
+
existing = await cfg.backend.get_trust_record(skill_name)
|
|
52
|
+
trust = existing or TrustRecord(skill_name=skill_name)
|
|
53
|
+
|
|
54
|
+
if not runs:
|
|
55
|
+
trust.last_evaluated_at = time.time()
|
|
56
|
+
return trust
|
|
57
|
+
|
|
58
|
+
live = [r for r in runs if not r.is_shadow]
|
|
59
|
+
total = len(live)
|
|
60
|
+
ok = sum(1 for r in live if r.status == RunStatus.COMPLETED)
|
|
61
|
+
failed = sum(1 for r in live if r.status == RunStatus.FAILED)
|
|
62
|
+
shadow = sum(1 for r in runs if r.is_shadow)
|
|
63
|
+
|
|
64
|
+
trust.total_runs = total
|
|
65
|
+
trust.successful_runs= ok
|
|
66
|
+
trust.failed_runs = failed
|
|
67
|
+
trust.shadow_runs = shadow
|
|
68
|
+
|
|
69
|
+
durations = [r.duration_ms for r in runs if r.duration_ms]
|
|
70
|
+
trust.avg_duration_ms = sum(durations) / len(durations) if durations else 0.0
|
|
71
|
+
|
|
72
|
+
success_rate = ok / total if total > 0 else 0.0
|
|
73
|
+
|
|
74
|
+
approvals = trust.human_approvals
|
|
75
|
+
rejections = trust.human_rejections
|
|
76
|
+
reviews = approvals + rejections
|
|
77
|
+
approval_rate = approvals / reviews if reviews > 0 else 0.5
|
|
78
|
+
|
|
79
|
+
volume_bonus = min(total / 50.0, 1.0)
|
|
80
|
+
|
|
81
|
+
most_recent = max((r.completed_at or 0.0) for r in runs)
|
|
82
|
+
days_inactive = (time.time() - most_recent) / 86400 if most_recent else 30
|
|
83
|
+
recency = max(0.0, 1.0 - days_inactive / 14.0)
|
|
84
|
+
|
|
85
|
+
score = (
|
|
86
|
+
success_rate * WEIGHTS["success_rate"]
|
|
87
|
+
+ approval_rate * WEIGHTS["human_approval_rate"]
|
|
88
|
+
+ volume_bonus * WEIGHTS["volume_bonus"]
|
|
89
|
+
+ recency * WEIGHTS["recency_factor"]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
trust.trust_score = round(min(score, 1.0), 4)
|
|
93
|
+
trust.trust_level = self._to_level(trust.trust_score)
|
|
94
|
+
trust.last_evaluated_at = time.time()
|
|
95
|
+
trust.last_updated_at = time.time()
|
|
96
|
+
|
|
97
|
+
log.info("trust.evaluated", skill=skill_name, score=trust.trust_score,
|
|
98
|
+
level=trust.trust_level, success_rate=round(success_rate, 3))
|
|
99
|
+
return trust
|
|
100
|
+
|
|
101
|
+
def _to_level(self, score: float) -> TrustLevel:
|
|
102
|
+
for level, threshold in THRESHOLDS.items():
|
|
103
|
+
if score >= threshold:
|
|
104
|
+
return level
|
|
105
|
+
return TrustLevel.UNTRUSTED
|
|
106
|
+
|
|
107
|
+
def explain(self, trust: TrustRecord) -> str:
|
|
108
|
+
return "\n".join([
|
|
109
|
+
f"Trust: '{trust.skill_name}' → {trust.trust_score:.2f} ({trust.trust_level.value})",
|
|
110
|
+
f" Runs (30d): {trust.total_runs}",
|
|
111
|
+
f" Successful: {trust.successful_runs}",
|
|
112
|
+
f" Failed: {trust.failed_runs}",
|
|
113
|
+
f" Shadow runs: {trust.shadow_runs}",
|
|
114
|
+
f" Human approvals: {trust.human_approvals}",
|
|
115
|
+
f" Human rejections: {trust.human_rejections}",
|
|
116
|
+
f" Avg duration: {trust.avg_duration_ms:.0f}ms",
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TrustScore:
|
|
121
|
+
"""Convenience helpers for reading and updating trust records."""
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
async def get(skill_name: str) -> Optional[TrustRecord]:
|
|
125
|
+
return await get_config().backend.get_trust_record(skill_name)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
async def approve(skill_name: str, approved_by: str = "human") -> None:
|
|
129
|
+
backend = get_config().backend
|
|
130
|
+
trust = await backend.get_trust_record(skill_name) or TrustRecord(skill_name=skill_name)
|
|
131
|
+
trust.human_approvals += 1
|
|
132
|
+
trust.last_updated_at = time.time()
|
|
133
|
+
await backend.save_trust_record(trust)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
async def reject(skill_name: str, rejected_by: str = "human") -> None:
|
|
137
|
+
backend = get_config().backend
|
|
138
|
+
trust = await backend.get_trust_record(skill_name) or TrustRecord(skill_name=skill_name)
|
|
139
|
+
trust.human_rejections += 1
|
|
140
|
+
trust.last_updated_at = time.time()
|
|
141
|
+
await backend.save_trust_record(trust)
|