agent-memory-bridge 0.32.1__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.
- agent_mem_bridge/__init__.py +1 -0
- agent_mem_bridge/__main__.py +4 -0
- agent_mem_bridge/archive_snapshot.py +175 -0
- agent_mem_bridge/classifier.py +227 -0
- agent_mem_bridge/cli.py +1204 -0
- agent_mem_bridge/client_config.py +313 -0
- agent_mem_bridge/codex_rollout.py +833 -0
- agent_mem_bridge/command_provider.py +250 -0
- agent_mem_bridge/consolidation.py +989 -0
- agent_mem_bridge/context_evaluation.py +344 -0
- agent_mem_bridge/context_manifest.py +791 -0
- agent_mem_bridge/contradiction.py +82 -0
- agent_mem_bridge/cross_client_activation.py +271 -0
- agent_mem_bridge/current_release_contract.py +99 -0
- agent_mem_bridge/database_maintenance.py +769 -0
- agent_mem_bridge/durable_data_policy.py +86 -0
- agent_mem_bridge/dynamic_state.py +978 -0
- agent_mem_bridge/embedding_index.py +516 -0
- agent_mem_bridge/embedding_scheduler.py +268 -0
- agent_mem_bridge/enrichment_rules.py +53 -0
- agent_mem_bridge/evidence_inspect.py +466 -0
- agent_mem_bridge/exporters.py +81 -0
- agent_mem_bridge/failure_report.py +152 -0
- agent_mem_bridge/filesystem_safety.py +70 -0
- agent_mem_bridge/first_run.py +320 -0
- agent_mem_bridge/governance_trigger.py +185 -0
- agent_mem_bridge/healthcheck.py +218 -0
- agent_mem_bridge/index_health.py +144 -0
- agent_mem_bridge/knowledge_explorer.py +911 -0
- agent_mem_bridge/learning_candidates.py +297 -0
- agent_mem_bridge/learning_policy.py +255 -0
- agent_mem_bridge/lineage.py +174 -0
- agent_mem_bridge/log_maintenance.py +35 -0
- agent_mem_bridge/mcp_boundary.py +196 -0
- agent_mem_bridge/onboarding.py +531 -0
- agent_mem_bridge/onboarding_contract.py +281 -0
- agent_mem_bridge/paths.py +779 -0
- agent_mem_bridge/poll_cursor.py +67 -0
- agent_mem_bridge/procedure_governance.py +248 -0
- agent_mem_bridge/profile_assembly.py +146 -0
- agent_mem_bridge/profile_bundle.py +144 -0
- agent_mem_bridge/profile_migration.py +417 -0
- agent_mem_bridge/project_init.py +229 -0
- agent_mem_bridge/promotion.py +264 -0
- agent_mem_bridge/promotion_governance.py +493 -0
- agent_mem_bridge/proof.py +375 -0
- agent_mem_bridge/provenance.py +68 -0
- agent_mem_bridge/public_surface.py +214 -0
- agent_mem_bridge/query.py +1137 -0
- agent_mem_bridge/recall_eligibility.py +164 -0
- agent_mem_bridge/recall_first.py +312 -0
- agent_mem_bridge/record_projection.py +438 -0
- agent_mem_bridge/reflex.py +1098 -0
- agent_mem_bridge/relation_metadata.py +88 -0
- agent_mem_bridge/release_contract.py +1791 -0
- agent_mem_bridge/repository.py +966 -0
- agent_mem_bridge/repository_bootstrap.py +306 -0
- agent_mem_bridge/repository_snapshot_store.py +371 -0
- agent_mem_bridge/retrieval_feedback.py +1059 -0
- agent_mem_bridge/review_queue.py +472 -0
- agent_mem_bridge/review_workflow.py +252 -0
- agent_mem_bridge/revisions.py +342 -0
- agent_mem_bridge/run_consolidation.py +1105 -0
- agent_mem_bridge/run_ledger.py +2422 -0
- agent_mem_bridge/run_outcome_authority.py +39 -0
- agent_mem_bridge/run_projection.py +735 -0
- agent_mem_bridge/run_verification_receipts.py +404 -0
- agent_mem_bridge/schema.py +2793 -0
- agent_mem_bridge/server.py +1338 -0
- agent_mem_bridge/service.py +321 -0
- agent_mem_bridge/service_health.py +94 -0
- agent_mem_bridge/service_lock.py +166 -0
- agent_mem_bridge/session_closeout.py +113 -0
- agent_mem_bridge/setup_apply.py +921 -0
- agent_mem_bridge/setup_planner.py +473 -0
- agent_mem_bridge/signals.py +703 -0
- agent_mem_bridge/source_sync.py +137 -0
- agent_mem_bridge/state_io.py +42 -0
- agent_mem_bridge/stdio_probe.py +441 -0
- agent_mem_bridge/storage.py +1204 -0
- agent_mem_bridge/structured_record.py +111 -0
- agent_mem_bridge/sync_notes.py +116 -0
- agent_mem_bridge/task_brief.py +551 -0
- agent_mem_bridge/task_memory.py +1407 -0
- agent_mem_bridge/telemetry.py +224 -0
- agent_mem_bridge/telemetry_summary.py +287 -0
- agent_mem_bridge/watcher.py +597 -0
- agent_mem_bridge/watcher_health.py +90 -0
- agent_memory_bridge-0.32.1.dist-info/METADATA +236 -0
- agent_memory_bridge-0.32.1.dist-info/RECORD +94 -0
- agent_memory_bridge-0.32.1.dist-info/WHEEL +5 -0
- agent_memory_bridge-0.32.1.dist-info/entry_points.txt +2 -0
- agent_memory_bridge-0.32.1.dist-info/licenses/LICENSE +21 -0
- agent_memory_bridge-0.32.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""agent-memory-bridge package."""
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from shutil import copy2
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
ROOT_FILES = (
|
|
10
|
+
"HOW-TO-USE-COLE.md",
|
|
11
|
+
"architecture.md",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
MEMORY_ROOT_FILES = (
|
|
15
|
+
"memory/.claude-memory-guard.md",
|
|
16
|
+
"memory/MEMORY.md",
|
|
17
|
+
"memory/QUEUE.md",
|
|
18
|
+
"memory/REDLINE.md",
|
|
19
|
+
"memory/status.md",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
SNAPSHOT_DIRS = (
|
|
23
|
+
"memory/core",
|
|
24
|
+
"memory/team",
|
|
25
|
+
"memory/workflows",
|
|
26
|
+
"memory/workspace",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
LIVE_SOURCE_FILES = (
|
|
30
|
+
"HOW-TO-USE-COLE.md",
|
|
31
|
+
"memory/.claude-memory-guard.md",
|
|
32
|
+
"memory/MEMORY.md",
|
|
33
|
+
"memory/QUEUE.md",
|
|
34
|
+
"memory/REDLINE.md",
|
|
35
|
+
"memory/core/core.md",
|
|
36
|
+
"memory/core/decision-making.md",
|
|
37
|
+
"memory/core/persona.md",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_default_snapshot_root(source_root: Path, *, timestamp: datetime | None = None) -> Path:
|
|
42
|
+
moment = timestamp or datetime.now(UTC)
|
|
43
|
+
stamp = moment.strftime("%Y-%m-%d-source-snapshot")
|
|
44
|
+
return Path(source_root).resolve() / "archive" / stamp
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_default_live_manifest_path(source_root: Path) -> Path:
|
|
48
|
+
return Path(source_root).resolve() / "live-source-manifest.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_profile_archive_snapshot(
|
|
52
|
+
source_root: Path,
|
|
53
|
+
snapshot_root: Path,
|
|
54
|
+
*,
|
|
55
|
+
compare_report: dict[str, Any] | None = None,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
source_root = Path(source_root).resolve()
|
|
58
|
+
snapshot_root = Path(snapshot_root).resolve()
|
|
59
|
+
copied_files: list[str] = []
|
|
60
|
+
|
|
61
|
+
for relative_path in iter_snapshot_relative_paths(source_root):
|
|
62
|
+
source_path = source_root / relative_path
|
|
63
|
+
target_path = snapshot_root / relative_path
|
|
64
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
copy2(source_path, target_path)
|
|
66
|
+
copied_files.append(relative_path.as_posix())
|
|
67
|
+
|
|
68
|
+
manifest = {
|
|
69
|
+
"created_at": datetime.now(UTC).isoformat(),
|
|
70
|
+
"source_root": str(source_root),
|
|
71
|
+
"snapshot_root": str(snapshot_root),
|
|
72
|
+
"file_count": len(copied_files),
|
|
73
|
+
"files": copied_files,
|
|
74
|
+
"compare_report": compare_report,
|
|
75
|
+
"migration_safe": bool(
|
|
76
|
+
compare_report
|
|
77
|
+
and compare_report.get("missing_count") == 0
|
|
78
|
+
and compare_report.get("extra_count") == 0
|
|
79
|
+
and compare_report.get("content_mismatch_count") == 0
|
|
80
|
+
and compare_report.get("namespace_mismatch_count") == 0
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
manifest_path = snapshot_root / "manifest.json"
|
|
84
|
+
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
85
|
+
manifest["manifest_path"] = str(manifest_path)
|
|
86
|
+
return manifest
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Legacy compatibility alias for older migration helpers.
|
|
90
|
+
create_cole_archive_snapshot = create_profile_archive_snapshot
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_live_source_manifest(
|
|
94
|
+
source_root: Path,
|
|
95
|
+
manifest_path: Path,
|
|
96
|
+
*,
|
|
97
|
+
relative_paths: list[Path] | None = None,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
source_root = Path(source_root).resolve()
|
|
100
|
+
manifest_path = Path(manifest_path).resolve()
|
|
101
|
+
files = relative_paths or iter_live_source_relative_paths(source_root)
|
|
102
|
+
rendered_files = [path.as_posix() for path in files]
|
|
103
|
+
manifest = {
|
|
104
|
+
"created_at": datetime.now(UTC).isoformat(),
|
|
105
|
+
"source_root": str(source_root),
|
|
106
|
+
"manifest_path": str(manifest_path),
|
|
107
|
+
"file_count": len(rendered_files),
|
|
108
|
+
"files": rendered_files,
|
|
109
|
+
}
|
|
110
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
112
|
+
return manifest
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def iter_snapshot_relative_paths(source_root: Path) -> list[Path]:
|
|
116
|
+
root = Path(source_root).resolve()
|
|
117
|
+
files: list[Path] = []
|
|
118
|
+
|
|
119
|
+
for relative in ROOT_FILES:
|
|
120
|
+
candidate = root / relative
|
|
121
|
+
if candidate.is_file():
|
|
122
|
+
files.append(Path(relative))
|
|
123
|
+
|
|
124
|
+
for relative in MEMORY_ROOT_FILES:
|
|
125
|
+
candidate = root / relative
|
|
126
|
+
if candidate.is_file():
|
|
127
|
+
files.append(Path(relative))
|
|
128
|
+
|
|
129
|
+
for relative_dir in SNAPSHOT_DIRS:
|
|
130
|
+
directory = root / relative_dir
|
|
131
|
+
if not directory.is_dir():
|
|
132
|
+
continue
|
|
133
|
+
for path in sorted(directory.rglob("*.md")):
|
|
134
|
+
files.append(path.relative_to(root))
|
|
135
|
+
|
|
136
|
+
deduped: list[Path] = []
|
|
137
|
+
seen: set[Path] = set()
|
|
138
|
+
for relative_path in files:
|
|
139
|
+
if relative_path in seen:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(relative_path)
|
|
142
|
+
deduped.append(relative_path)
|
|
143
|
+
return deduped
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def iter_live_source_relative_paths(source_root: Path) -> list[Path]:
|
|
147
|
+
root = Path(source_root).resolve()
|
|
148
|
+
files: list[Path] = []
|
|
149
|
+
for relative in LIVE_SOURCE_FILES:
|
|
150
|
+
candidate = root / relative
|
|
151
|
+
if candidate.is_file():
|
|
152
|
+
files.append(Path(relative))
|
|
153
|
+
return files
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_manifest_relative_paths(manifest_path: Path) -> list[Path]:
|
|
157
|
+
manifest = load_manifest(manifest_path)
|
|
158
|
+
return [Path(item) for item in manifest.get("files", [])]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def load_manifest(manifest_path: Path) -> dict[str, Any]:
|
|
162
|
+
path = Path(manifest_path).resolve()
|
|
163
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
164
|
+
data = json.load(handle)
|
|
165
|
+
return data if isinstance(data, dict) else {}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def find_latest_snapshot_manifest(source_root: Path) -> Path | None:
|
|
169
|
+
archive_root = Path(source_root).resolve() / "archive"
|
|
170
|
+
if not archive_root.is_dir():
|
|
171
|
+
return None
|
|
172
|
+
manifests = sorted(archive_root.glob("*-source-snapshot/manifest.json"))
|
|
173
|
+
if not manifests:
|
|
174
|
+
return None
|
|
175
|
+
return manifests[-1]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from .command_provider import CommandLimits, CommandProviderError, run_json_command
|
|
9
|
+
|
|
10
|
+
ClassifierMode = Literal["off", "shadow", "assist"]
|
|
11
|
+
ClassifierProvider = Literal["command"]
|
|
12
|
+
ALLOWED_CLASSIFIER_TAG_PREFIXES = ("domain:", "topic:")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class ClassifierConfig:
|
|
17
|
+
mode: ClassifierMode = "off"
|
|
18
|
+
provider: ClassifierProvider = "command"
|
|
19
|
+
command: str | tuple[str, ...] = ""
|
|
20
|
+
timeout_seconds: float = 10.0
|
|
21
|
+
batch_size: int = 16
|
|
22
|
+
minimum_confidence: float = 0.6
|
|
23
|
+
trusted_shell: bool = False
|
|
24
|
+
max_input_bytes: int = 1_000_000
|
|
25
|
+
max_output_bytes: int = 2_000_000
|
|
26
|
+
max_stderr_bytes: int = 65_536
|
|
27
|
+
env_allowlist: tuple[str, ...] = ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class EnrichmentCandidate:
|
|
32
|
+
key: str
|
|
33
|
+
text: str
|
|
34
|
+
fallback_tags: tuple[str, ...] = ()
|
|
35
|
+
title: str = ""
|
|
36
|
+
source_id: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Classification:
|
|
41
|
+
key: str
|
|
42
|
+
tags: tuple[str, ...]
|
|
43
|
+
domains: tuple[str, ...]
|
|
44
|
+
topics: tuple[str, ...]
|
|
45
|
+
classifier_suggested_tags: tuple[str, ...] = ()
|
|
46
|
+
confidence: float | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ClassificationBatchOutcome:
|
|
51
|
+
predictions: dict[str, Classification]
|
|
52
|
+
requested_count: int
|
|
53
|
+
error: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EnrichmentClassifier:
|
|
57
|
+
def __init__(self, config: ClassifierConfig) -> None:
|
|
58
|
+
if not math.isfinite(config.minimum_confidence) or not 0.0 <= config.minimum_confidence <= 1.0:
|
|
59
|
+
raise ValueError("minimum_confidence must be a finite value between 0 and 1")
|
|
60
|
+
self.config = config
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def active(self) -> bool:
|
|
64
|
+
command_configured = (
|
|
65
|
+
bool(self.config.command.strip()) if isinstance(self.config.command, str) else bool(self.config.command)
|
|
66
|
+
)
|
|
67
|
+
return self.config.mode != "off" and command_configured
|
|
68
|
+
|
|
69
|
+
def classify(self, candidates: list[EnrichmentCandidate]) -> ClassificationBatchOutcome:
|
|
70
|
+
if not candidates:
|
|
71
|
+
return ClassificationBatchOutcome(predictions={}, requested_count=0)
|
|
72
|
+
if not self.active:
|
|
73
|
+
return ClassificationBatchOutcome(predictions={}, requested_count=len(candidates))
|
|
74
|
+
|
|
75
|
+
predictions: dict[str, Classification] = {}
|
|
76
|
+
for batch in self._batched(candidates):
|
|
77
|
+
outcome = self._classify_batch(batch)
|
|
78
|
+
predictions.update(outcome.predictions)
|
|
79
|
+
if outcome.error:
|
|
80
|
+
return ClassificationBatchOutcome(
|
|
81
|
+
predictions=predictions,
|
|
82
|
+
requested_count=len(candidates),
|
|
83
|
+
error=outcome.error,
|
|
84
|
+
)
|
|
85
|
+
return ClassificationBatchOutcome(predictions=predictions, requested_count=len(candidates))
|
|
86
|
+
|
|
87
|
+
def accepted_tags(self, classification: Classification | None) -> list[str]:
|
|
88
|
+
if classification is None:
|
|
89
|
+
return []
|
|
90
|
+
confidence = classification.confidence
|
|
91
|
+
if confidence is None or not math.isfinite(confidence) or not 0.0 <= confidence <= 1.0:
|
|
92
|
+
return []
|
|
93
|
+
if confidence < self.config.minimum_confidence:
|
|
94
|
+
return []
|
|
95
|
+
return self._policy_tags(list(classification.tags))
|
|
96
|
+
|
|
97
|
+
def _classify_batch(self, batch: list[EnrichmentCandidate]) -> ClassificationBatchOutcome:
|
|
98
|
+
payload = {
|
|
99
|
+
"items": [
|
|
100
|
+
{
|
|
101
|
+
"key": candidate.key,
|
|
102
|
+
"text": candidate.text,
|
|
103
|
+
"title": candidate.title,
|
|
104
|
+
"source_id": candidate.source_id,
|
|
105
|
+
"fallback_tags": list(candidate.fallback_tags),
|
|
106
|
+
}
|
|
107
|
+
for candidate in batch
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
try:
|
|
111
|
+
completed = run_json_command(
|
|
112
|
+
self.config.command,
|
|
113
|
+
payload,
|
|
114
|
+
timeout_seconds=self.config.timeout_seconds,
|
|
115
|
+
trusted_shell=self.config.trusted_shell,
|
|
116
|
+
limits=CommandLimits(
|
|
117
|
+
max_input_bytes=self.config.max_input_bytes,
|
|
118
|
+
max_stdout_bytes=self.config.max_output_bytes,
|
|
119
|
+
max_stderr_bytes=self.config.max_stderr_bytes,
|
|
120
|
+
),
|
|
121
|
+
env_allowlist=self.config.env_allowlist,
|
|
122
|
+
)
|
|
123
|
+
except CommandProviderError as exc:
|
|
124
|
+
return ClassificationBatchOutcome(
|
|
125
|
+
predictions={},
|
|
126
|
+
requested_count=len(batch),
|
|
127
|
+
error=f"classifier command failed: {exc}",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if completed.returncode != 0:
|
|
131
|
+
return ClassificationBatchOutcome(
|
|
132
|
+
predictions={},
|
|
133
|
+
requested_count=len(batch),
|
|
134
|
+
error=(
|
|
135
|
+
f"classifier command failed with exit code {completed.returncode} "
|
|
136
|
+
f"(fingerprint={completed.fingerprint})"
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
raw = json.loads(completed.stdout)
|
|
142
|
+
except json.JSONDecodeError as exc:
|
|
143
|
+
return ClassificationBatchOutcome(
|
|
144
|
+
predictions={},
|
|
145
|
+
requested_count=len(batch),
|
|
146
|
+
error=f"classifier returned invalid JSON ({exc.__class__.__name__})",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
items = raw.get("items", raw) if isinstance(raw, dict) else raw
|
|
150
|
+
if not isinstance(items, list):
|
|
151
|
+
return ClassificationBatchOutcome(
|
|
152
|
+
predictions={},
|
|
153
|
+
requested_count=len(batch),
|
|
154
|
+
error="classifier output must be a list or an object with an items list",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
predictions: dict[str, Classification] = {}
|
|
158
|
+
for item in items:
|
|
159
|
+
if not isinstance(item, dict):
|
|
160
|
+
continue
|
|
161
|
+
key = str(item.get("key", "")).strip()
|
|
162
|
+
if not key:
|
|
163
|
+
continue
|
|
164
|
+
classifier_suggested_tags = self._normalize_tags(item)
|
|
165
|
+
tags = self._policy_tags(classifier_suggested_tags)
|
|
166
|
+
predictions[key] = Classification(
|
|
167
|
+
key=key,
|
|
168
|
+
tags=tuple(tags),
|
|
169
|
+
domains=tuple(tag for tag in tags if tag.startswith("domain:")),
|
|
170
|
+
topics=tuple(tag for tag in tags if tag.startswith("topic:")),
|
|
171
|
+
classifier_suggested_tags=tuple(classifier_suggested_tags),
|
|
172
|
+
confidence=self._normalize_confidence(item.get("confidence")),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return ClassificationBatchOutcome(predictions=predictions, requested_count=len(batch))
|
|
176
|
+
|
|
177
|
+
def _normalize_tags(self, item: dict[str, Any]) -> list[str]:
|
|
178
|
+
tags: list[str] = []
|
|
179
|
+
raw_tags = item.get("tags", [])
|
|
180
|
+
tag_values = raw_tags if isinstance(raw_tags, (list, tuple)) else ()
|
|
181
|
+
for value in tag_values:
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
normalized = value.strip()
|
|
184
|
+
if normalized:
|
|
185
|
+
tags.append(normalized)
|
|
186
|
+
for field, prefix in (("domains", "domain:"), ("topics", "topic:")):
|
|
187
|
+
raw_values = item.get(field, [])
|
|
188
|
+
values = raw_values if isinstance(raw_values, (list, tuple)) else ()
|
|
189
|
+
for value in values:
|
|
190
|
+
if not isinstance(value, str):
|
|
191
|
+
continue
|
|
192
|
+
normalized = value.strip()
|
|
193
|
+
if not normalized:
|
|
194
|
+
continue
|
|
195
|
+
tags.append(normalized if normalized.startswith(prefix) else f"{prefix}{normalized}")
|
|
196
|
+
seen: set[str] = set()
|
|
197
|
+
result: list[str] = []
|
|
198
|
+
for tag in tags:
|
|
199
|
+
if tag in seen:
|
|
200
|
+
continue
|
|
201
|
+
seen.add(tag)
|
|
202
|
+
result.append(tag)
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _policy_tags(tags: list[str]) -> list[str]:
|
|
207
|
+
return [
|
|
208
|
+
tag
|
|
209
|
+
for tag in tags
|
|
210
|
+
if any(tag.startswith(prefix) and len(tag) > len(prefix) for prefix in ALLOWED_CLASSIFIER_TAG_PREFIXES)
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _normalize_confidence(value: Any) -> float | None:
|
|
215
|
+
if value is None:
|
|
216
|
+
return None
|
|
217
|
+
try:
|
|
218
|
+
normalized = float(value)
|
|
219
|
+
except (TypeError, ValueError):
|
|
220
|
+
return None
|
|
221
|
+
if not math.isfinite(normalized) or not 0.0 <= normalized <= 1.0:
|
|
222
|
+
return None
|
|
223
|
+
return normalized
|
|
224
|
+
|
|
225
|
+
def _batched(self, candidates: list[EnrichmentCandidate]) -> list[list[EnrichmentCandidate]]:
|
|
226
|
+
batch_size = max(1, int(self.config.batch_size))
|
|
227
|
+
return [candidates[index : index + batch_size] for index in range(0, len(candidates), batch_size)]
|