zer0lint 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.
- zer0lint/__init__.py +8 -0
- zer0lint/__main__.py +6 -0
- zer0lint/analyzer.py +73 -0
- zer0lint/cli.py +182 -0
- zer0lint/fixer.py +116 -0
- zer0lint/orchestrator.py +240 -0
- zer0lint/sampler.py +165 -0
- zer0lint/scanner.py +135 -0
- zer0lint/tester.py +316 -0
- zer0lint-0.1.0.dist-info/METADATA +185 -0
- zer0lint-0.1.0.dist-info/RECORD +13 -0
- zer0lint-0.1.0.dist-info/WHEEL +4 -0
- zer0lint-0.1.0.dist-info/entry_points.txt +2 -0
zer0lint/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""zer0lint — mem0 extraction optimizer."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__author__ = "Hermes Labs"
|
|
5
|
+
__description__ = (
|
|
6
|
+
"Diagnostic and optimization tool for mem0 extraction pipelines. "
|
|
7
|
+
"Inspects your system, generates custom extraction prompts, tests them."
|
|
8
|
+
)
|
zer0lint/__main__.py
ADDED
zer0lint/analyzer.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Generate extraction prompt from environment signals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def analyze_with_llm(llm: object, environment_summary: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Use LLM to generate a domain-specific extraction prompt from environment signals.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
llm: mem0's configured LLM instance (any provider)
|
|
14
|
+
environment_summary: Text describing the system environment (from scanner)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A domain-specific extraction prompt tailored to the environment
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
analysis_prompt = f"""You are an expert at designing memory extraction prompts for AI agents.
|
|
21
|
+
|
|
22
|
+
Here is a description of the system environment you're designing a prompt for:
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
{environment_summary}
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Based on this environment, write a mem0 custom_fact_extraction_prompt that will capture
|
|
29
|
+
the most important facts this system needs to remember.
|
|
30
|
+
|
|
31
|
+
The prompt should:
|
|
32
|
+
1. Identify 5-8 specific categories of facts this system deals with
|
|
33
|
+
2. Be specific to the actual work (not generic)
|
|
34
|
+
3. Include 2-3 concrete examples (Input → Output in JSON)
|
|
35
|
+
4. End with: Return facts as JSON with key "facts" and a list of strings. Extract generously.
|
|
36
|
+
|
|
37
|
+
Return ONLY the prompt text. No explanation, no markdown, no preamble. Start writing now:"""
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
response = llm.chat_completion(
|
|
41
|
+
messages=[{"role": "user", "content": analysis_prompt}],
|
|
42
|
+
temperature=0.3,
|
|
43
|
+
)
|
|
44
|
+
if isinstance(response, dict):
|
|
45
|
+
prompt_text = response.get("message", response.get("content", ""))
|
|
46
|
+
else:
|
|
47
|
+
prompt_text = str(response)
|
|
48
|
+
return prompt_text.strip()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise RuntimeError(f"LLM analysis failed: {e}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fallback_prompt_from_patterns(patterns: dict[str, list[str]]) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Generate a basic extraction prompt from detected patterns when LLM is unavailable.
|
|
56
|
+
"""
|
|
57
|
+
if not patterns:
|
|
58
|
+
return (
|
|
59
|
+
"Extract all factual information from the conversation. "
|
|
60
|
+
"Return as JSON with key 'facts' and a list of strings. Extract generously."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
categories_text = "\n".join(
|
|
64
|
+
f"{i+1}. {cat.capitalize()}: {', '.join(words)}"
|
|
65
|
+
for i, (cat, words) in enumerate(patterns.items())
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return f"""You are a memory organizer for a system that works with {', '.join(patterns.keys())} data.
|
|
69
|
+
|
|
70
|
+
Extract ALL significant facts from the conversation. Focus on:
|
|
71
|
+
{categories_text}
|
|
72
|
+
|
|
73
|
+
Return facts as JSON with key "facts" and a list of strings. Extract generously."""
|
zer0lint/cli.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""CLI for zer0lint v0.2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.syntax import Syntax
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from zer0lint import __version__
|
|
17
|
+
from zer0lint.fixer import detect_extraction_model
|
|
18
|
+
from zer0lint.orchestrator import run_check, run_generate
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
err_console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(help="zer0lint — AI memory extraction diagnostics")
|
|
24
|
+
|
|
25
|
+
DEFAULT_CONFIG_CANDIDATES = [
|
|
26
|
+
Path.home() / ".mem0" / "config.json",
|
|
27
|
+
Path.home() / ".mem0_config.json",
|
|
28
|
+
Path("config.json"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_config(config_path: Optional[str]) -> tuple[dict, Optional[Path]]:
|
|
33
|
+
"""Load mem0 config from path or auto-detect. Returns (config_dict, resolved_path)."""
|
|
34
|
+
if config_path:
|
|
35
|
+
p = Path(config_path)
|
|
36
|
+
if not p.exists():
|
|
37
|
+
err_console.print(f"[red]Config not found:[/red] {p}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
else:
|
|
40
|
+
p = next((c for c in DEFAULT_CONFIG_CANDIDATES if c.exists()), None)
|
|
41
|
+
if not p:
|
|
42
|
+
err_console.print(
|
|
43
|
+
"[red]No config found.[/red] Provide with --config (e.g., --config ~/.mem0/config.json)"
|
|
44
|
+
)
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(p) as f:
|
|
49
|
+
return json.load(f), p
|
|
50
|
+
except Exception as e:
|
|
51
|
+
err_console.print(f"[red]Error reading config:[/red] {e}")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def check(
|
|
57
|
+
config_path: Optional[str] = typer.Option(None, "--config", help="Path to mem0 config.json"),
|
|
58
|
+
verbose: bool = typer.Option(False, "--verbose", "-v"),
|
|
59
|
+
n: int = typer.Option(5, "--facts", "-n", help="Number of test facts"),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Check your current mem0 extraction pipeline health.
|
|
63
|
+
|
|
64
|
+
Tests your config as-is with N synthetic domain facts.
|
|
65
|
+
Shows recall score and status (HEALTHY / ACCEPTABLE / DEGRADED / CRITICAL).
|
|
66
|
+
"""
|
|
67
|
+
config_dict, resolved = _load_config(config_path)
|
|
68
|
+
model = detect_extraction_model(config_dict)
|
|
69
|
+
has_custom = bool(config_dict.get("custom_fact_extraction_prompt"))
|
|
70
|
+
|
|
71
|
+
console.print(f"\n[bold]zer0lint v{__version__} — extraction health check[/bold]")
|
|
72
|
+
console.print(f"Config : {resolved}")
|
|
73
|
+
console.print(f"Model : {model}")
|
|
74
|
+
console.print(f"Prompt : {'custom' if has_custom else 'default (mem0 built-in)'}\n")
|
|
75
|
+
|
|
76
|
+
result = run_check(config_dict, verbose=verbose, n_facts=n)
|
|
77
|
+
|
|
78
|
+
color = {"HEALTHY": "green", "ACCEPTABLE": "cyan", "DEGRADED": "yellow", "CRITICAL": "red"}.get(
|
|
79
|
+
result["status"], "white"
|
|
80
|
+
)
|
|
81
|
+
console.print(
|
|
82
|
+
f"Score : [bold {color}]{result['score']}/{result['total']} ({result['pct']:.0f}%) — {result['status']}[/bold {color}]\n"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not verbose:
|
|
86
|
+
for d in result["details"]:
|
|
87
|
+
icon = "✅" if d["found"] else ("⚠ " if d.get("stored") else "❌")
|
|
88
|
+
console.print(f" {icon} {d['label']}")
|
|
89
|
+
|
|
90
|
+
if result["status"] in ("DEGRADED", "CRITICAL"):
|
|
91
|
+
console.print(
|
|
92
|
+
"\n[yellow]Run [bold]zer0lint generate[/bold] to diagnose and fix.[/yellow]"
|
|
93
|
+
)
|
|
94
|
+
elif result["status"] == "ACCEPTABLE":
|
|
95
|
+
console.print("\n[cyan]Run [bold]zer0lint generate[/bold] to try improving to 5/5.[/cyan]")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command()
|
|
99
|
+
def generate(
|
|
100
|
+
config_path: Optional[str] = typer.Option(None, "--config", help="Path to mem0 config.json"),
|
|
101
|
+
verbose: bool = typer.Option(True, "--verbose/--quiet", "-v/-q"),
|
|
102
|
+
apply: bool = typer.Option(True, "--apply/--dry-run", help="Apply fix to config"),
|
|
103
|
+
n: int = typer.Option(5, "--facts", "-n", help="Number of test facts"),
|
|
104
|
+
) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Diagnose and fix your mem0 extraction pipeline.
|
|
107
|
+
|
|
108
|
+
Runs three phases:
|
|
109
|
+
1. Baseline recall test (current config)
|
|
110
|
+
2. Re-test with zer0lint technical extraction prompt (config-level)
|
|
111
|
+
3. If improved → write validated prompt to your config
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
zer0lint generate --config ~/.mem0/config.json
|
|
115
|
+
zer0lint generate --config ~/.mem0/config.json --dry-run
|
|
116
|
+
"""
|
|
117
|
+
config_dict, resolved = _load_config(config_path)
|
|
118
|
+
model = detect_extraction_model(config_dict)
|
|
119
|
+
has_custom = bool(config_dict.get("custom_fact_extraction_prompt"))
|
|
120
|
+
|
|
121
|
+
console.print(f"\n[bold]zer0lint v{__version__} — extraction optimizer[/bold]")
|
|
122
|
+
console.print(f"Config : {resolved}")
|
|
123
|
+
console.print(f"Model : {model}")
|
|
124
|
+
console.print(f"Prompt : {'custom' if has_custom else 'default (mem0 built-in)'}")
|
|
125
|
+
if not apply:
|
|
126
|
+
console.print("[yellow]Mode : dry-run (will not write to config)[/yellow]")
|
|
127
|
+
console.print()
|
|
128
|
+
|
|
129
|
+
result = run_generate(
|
|
130
|
+
base_config=config_dict,
|
|
131
|
+
config_path=resolved if apply else None,
|
|
132
|
+
verbose=verbose,
|
|
133
|
+
n_facts=n,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not result["success"]:
|
|
137
|
+
err_console.print("[red]✗ Generate failed.[/red]")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
if result.get("verdict") == "already_healthy":
|
|
141
|
+
console.print("\n[green]✅ Your extraction is already at 100%. No changes needed.[/green]")
|
|
142
|
+
raise typer.Exit(0)
|
|
143
|
+
|
|
144
|
+
# Show before/after
|
|
145
|
+
init_pct = result.get("initial_pct", 0)
|
|
146
|
+
impr_pct = result.get("improved_pct", 0)
|
|
147
|
+
imp_pp = result.get("improvement_pp", 0)
|
|
148
|
+
|
|
149
|
+
console.print(f"\n[bold]Results:[/bold]")
|
|
150
|
+
console.print(f" Before : {result['initial_score']}/{result.get('total', 5) if 'total' in result else 5} ({init_pct:.0f}%)")
|
|
151
|
+
console.print(f" After : {result['improved_score']}/{result.get('total', 5) if 'total' in result else 5} ({impr_pct:.0f}%)")
|
|
152
|
+
imp_color = "green" if imp_pp > 0 else "red"
|
|
153
|
+
console.print(f" Δ : [{imp_color}]{imp_pp:+.0f}pp[/{imp_color}]")
|
|
154
|
+
|
|
155
|
+
verdict = result.get("verdict")
|
|
156
|
+
if verdict == "improved" and result.get("applied"):
|
|
157
|
+
console.print(f"\n[green]✅ Fix applied to config.[/green]")
|
|
158
|
+
if result.get("backup_path"):
|
|
159
|
+
console.print(f" Backup: {result['backup_path']}")
|
|
160
|
+
console.print("\n[dim]Restart your agent to pick up the new extraction prompt.[/dim]")
|
|
161
|
+
elif verdict == "improved" and not result.get("applied"):
|
|
162
|
+
console.print(f"\n[cyan]Would improve by {imp_pp:+.0f}pp — run without --dry-run to apply.[/cyan]")
|
|
163
|
+
elif verdict == "no_improvement":
|
|
164
|
+
console.print(f"\n[yellow]⚠ zer0lint prompt did not improve recall on this config.[/yellow]")
|
|
165
|
+
console.print("[dim]Your current setup may already be optimized, or a different domain prompt is needed.[/dim]")
|
|
166
|
+
elif verdict == "below_threshold":
|
|
167
|
+
console.print(f"\n[yellow]⚠ Improvement detected but below threshold — not applying automatically.[/yellow]")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.callback(invoke_without_command=True)
|
|
171
|
+
def version_cb(
|
|
172
|
+
show_version: bool = typer.Option(None, "--version", is_eager=True, help="Show version"),
|
|
173
|
+
ctx: typer.Context = typer.Context,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""zer0lint — AI memory extraction diagnostics."""
|
|
176
|
+
if show_version:
|
|
177
|
+
console.print(f"zer0lint v{__version__}")
|
|
178
|
+
raise typer.Exit(0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
app()
|
zer0lint/fixer.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Apply generated extraction prompts to mem0 config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def backup_config(config_path: str | Path) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Backup the current config before modifying.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config_path: Path to mem0 config file
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Path to backup file
|
|
21
|
+
"""
|
|
22
|
+
config_path = Path(config_path)
|
|
23
|
+
backup_path = config_path.parent / f"{config_path.stem}.backup.{datetime.now().isoformat()}"
|
|
24
|
+
shutil.copy2(config_path, backup_path)
|
|
25
|
+
return str(backup_path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def apply_prompt(
|
|
29
|
+
config_path: str | Path, new_prompt: str, backup: bool = True
|
|
30
|
+
) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
Apply a new extraction prompt to mem0 config file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config_path: Path to mem0 config.json
|
|
36
|
+
new_prompt: The new extraction prompt to set
|
|
37
|
+
backup: Whether to backup the original config (default True)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict with keys: success (bool), backup_path (str), config_path (str), changes (dict)
|
|
41
|
+
"""
|
|
42
|
+
config_path = Path(config_path)
|
|
43
|
+
|
|
44
|
+
if not config_path.exists():
|
|
45
|
+
raise FileNotFoundError(f"Config not found: {config_path}")
|
|
46
|
+
|
|
47
|
+
# Read current config
|
|
48
|
+
with open(config_path) as f:
|
|
49
|
+
config = json.load(f)
|
|
50
|
+
|
|
51
|
+
# Backup
|
|
52
|
+
backup_path = None
|
|
53
|
+
if backup:
|
|
54
|
+
backup_path = backup_config(config_path)
|
|
55
|
+
|
|
56
|
+
# Record the old prompt for comparison
|
|
57
|
+
old_prompt = config.get("custom_fact_extraction_prompt", "(none)")
|
|
58
|
+
|
|
59
|
+
# Apply new prompt
|
|
60
|
+
config["custom_fact_extraction_prompt"] = new_prompt
|
|
61
|
+
|
|
62
|
+
# Write back
|
|
63
|
+
with open(config_path, "w") as f:
|
|
64
|
+
json.dump(config, f, indent=2)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"success": True,
|
|
68
|
+
"config_path": str(config_path),
|
|
69
|
+
"backup_path": backup_path,
|
|
70
|
+
"changes": {
|
|
71
|
+
"field": "custom_fact_extraction_prompt",
|
|
72
|
+
"old_length": len(old_prompt),
|
|
73
|
+
"new_length": len(new_prompt),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_extraction_model(config: dict) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Detect which LLM is configured for extraction in mem0 config.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: Parsed mem0 config dict
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
String describing the extraction model (e.g., "mistral:7b", "gpt-4o", "unknown")
|
|
87
|
+
"""
|
|
88
|
+
# Most configs have an llm.config.model field
|
|
89
|
+
llm_config = config.get("llm", {})
|
|
90
|
+
if isinstance(llm_config, dict):
|
|
91
|
+
if "config" in llm_config:
|
|
92
|
+
model = llm_config["config"].get("model")
|
|
93
|
+
if model:
|
|
94
|
+
return model
|
|
95
|
+
if "model" in llm_config:
|
|
96
|
+
return llm_config["model"]
|
|
97
|
+
|
|
98
|
+
return "unknown"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def detect_vector_store(config: dict) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Detect which vector store is configured in mem0.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Parsed mem0 config dict
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
String describing the vector store (e.g., "chroma", "qdrant", "unknown")
|
|
110
|
+
"""
|
|
111
|
+
vs_config = config.get("vector_store", {})
|
|
112
|
+
if isinstance(vs_config, dict):
|
|
113
|
+
provider = vs_config.get("provider")
|
|
114
|
+
if provider:
|
|
115
|
+
return provider
|
|
116
|
+
return "unknown"
|
zer0lint/orchestrator.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Main orchestrator for zer0lint v0.2 — config-level injection, validated flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from zer0lint.fixer import apply_prompt, detect_extraction_model
|
|
11
|
+
from zer0lint.tester import (
|
|
12
|
+
generate_test_facts_for_categories,
|
|
13
|
+
validate_extraction_prompt,
|
|
14
|
+
cleanup_test_memories,
|
|
15
|
+
count_stored_memories,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# mem0's built-in default (personal assistant focused)
|
|
20
|
+
MEM0_DEFAULT_PROMPT = """You are a Personal Information Organizer. Extract facts from conversations.
|
|
21
|
+
Types: personal preferences, dates, relationships, activities, health, professional details.
|
|
22
|
+
Return as JSON: {"facts": ["fact1", ...]}
|
|
23
|
+
Input: Hi. Output: {"facts": []}"""
|
|
24
|
+
|
|
25
|
+
# zer0lint's technical-domain prompt (validated: 5/5 recall vs 2/5 with personal prompt)
|
|
26
|
+
TECHNICAL_EXTRACTION_PROMPT = """You are a Technical Memory Organizer for an AI agent workspace. Extract ALL factual statements from the input — technical decisions, infrastructure changes, product details, research findings, scores, dates, names, URLs, versions, and architectural choices.
|
|
27
|
+
|
|
28
|
+
Types of information to extract:
|
|
29
|
+
1. Infrastructure changes (services started/stopped, configs changed, versions installed)
|
|
30
|
+
2. Technical decisions and their rationale
|
|
31
|
+
3. Product/project details (names, versions, stars, URLs, status)
|
|
32
|
+
4. Research findings and experiment results (scores, percentages, benchmarks)
|
|
33
|
+
5. People, organizations, and relationships
|
|
34
|
+
6. Dates, deadlines, and timelines
|
|
35
|
+
7. File paths, port numbers, model names, and system specifics
|
|
36
|
+
8. Security findings and audit scores
|
|
37
|
+
9. Architecture patterns and design decisions
|
|
38
|
+
10. Task assignments and status changes
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
|
|
42
|
+
Input: We forked OpenClaw and installed it as v2026.3.14
|
|
43
|
+
Output: {"facts": ["Forked OpenClaw, installed as version 2026.3.14"]}
|
|
44
|
+
|
|
45
|
+
Input: The security audit scored 7.2 out of 10, up from 6.1
|
|
46
|
+
Output: {"facts": ["Security audit score: 7.2/10", "Previous security audit score was 6.1"]}
|
|
47
|
+
|
|
48
|
+
Input: Little Canary runs as HTTP server on port 18421 in full blocking mode
|
|
49
|
+
Output: {"facts": ["Little Canary runs as HTTP server on port 18421", "Little Canary is in full blocking mode"]}
|
|
50
|
+
|
|
51
|
+
Input: Hi, how are you?
|
|
52
|
+
Output: {"facts": []}
|
|
53
|
+
|
|
54
|
+
Return facts as JSON with key "facts" and a list of strings. Extract generously — it's better to capture too much than too little. Every concrete fact matters."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _make_memory(base_config: dict, custom_prompt: Optional[str] = None, collection_suffix: str = "test"):
|
|
58
|
+
"""Create a mem0 Memory instance with optional config-level prompt injection."""
|
|
59
|
+
from mem0 import Memory
|
|
60
|
+
|
|
61
|
+
config = json.loads(json.dumps(base_config)) # deep copy
|
|
62
|
+
|
|
63
|
+
# Swap collection to an isolated test namespace
|
|
64
|
+
if "vector_store" in config and "config" in config["vector_store"]:
|
|
65
|
+
orig_name = config["vector_store"]["config"].get("collection_name", "mem0")
|
|
66
|
+
config["vector_store"]["config"]["collection_name"] = f"{orig_name}_{collection_suffix}"
|
|
67
|
+
|
|
68
|
+
# Config-level prompt injection (the correct way in mem0 v1.x)
|
|
69
|
+
if custom_prompt:
|
|
70
|
+
config["custom_fact_extraction_prompt"] = custom_prompt
|
|
71
|
+
elif "custom_fact_extraction_prompt" in config:
|
|
72
|
+
del config["custom_fact_extraction_prompt"]
|
|
73
|
+
|
|
74
|
+
return Memory.from_config(config)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_check(
|
|
78
|
+
base_config: dict,
|
|
79
|
+
verbose: bool = False,
|
|
80
|
+
n_facts: int = 5,
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Phase 1: Baseline recall test.
|
|
84
|
+
Tests current config as-is against domain-relevant synthetic facts.
|
|
85
|
+
|
|
86
|
+
Returns dict: score, total, pct, status, details
|
|
87
|
+
"""
|
|
88
|
+
if verbose:
|
|
89
|
+
model = detect_extraction_model(base_config)
|
|
90
|
+
print(f"[CHECK] Using model: {model}")
|
|
91
|
+
print(f"[CHECK] Testing with {n_facts} synthetic facts...")
|
|
92
|
+
|
|
93
|
+
memory = _make_memory(base_config, collection_suffix="check")
|
|
94
|
+
uid = "zer0lint_check"
|
|
95
|
+
cleanup_test_memories(memory, user_id=uid)
|
|
96
|
+
|
|
97
|
+
facts = generate_test_facts_for_categories(["technical", "research"], count=n_facts)
|
|
98
|
+
results = validate_extraction_prompt(memory, facts, "", user_id=uid, wait_seconds=1.5)
|
|
99
|
+
|
|
100
|
+
score = results["score"]
|
|
101
|
+
total = results["total"]
|
|
102
|
+
pct = score / total * 100 if total > 0 else 0
|
|
103
|
+
|
|
104
|
+
if pct >= 80:
|
|
105
|
+
status = "HEALTHY"
|
|
106
|
+
elif pct >= 60:
|
|
107
|
+
status = "ACCEPTABLE"
|
|
108
|
+
elif pct >= 40:
|
|
109
|
+
status = "DEGRADED"
|
|
110
|
+
else:
|
|
111
|
+
status = "CRITICAL"
|
|
112
|
+
|
|
113
|
+
if verbose:
|
|
114
|
+
print(f"[CHECK] Score: {score}/{total} ({pct:.0f}%) — {status}")
|
|
115
|
+
for d in results["details"]:
|
|
116
|
+
icon = "✅" if d["found"] else ("⚠ " if d.get("stored") else "❌")
|
|
117
|
+
print(f" {icon} {d['label']}: {d['text'][:55]}...")
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"score": score,
|
|
121
|
+
"total": total,
|
|
122
|
+
"pct": pct,
|
|
123
|
+
"status": status,
|
|
124
|
+
"details": results["details"],
|
|
125
|
+
"failures": results["failures"],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def run_generate(
|
|
130
|
+
base_config: dict,
|
|
131
|
+
config_path: Optional[str | Path] = None,
|
|
132
|
+
verbose: bool = False,
|
|
133
|
+
n_facts: int = 5,
|
|
134
|
+
) -> dict:
|
|
135
|
+
"""
|
|
136
|
+
Full zer0lint v0.2 generate flow (3 phases):
|
|
137
|
+
Phase 1: Baseline recall test (current config)
|
|
138
|
+
Phase 2: Re-test with zer0lint technical prompt (config-level injection)
|
|
139
|
+
Phase 3: If improved → write to config
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
base_config: Parsed mem0 config dict
|
|
143
|
+
config_path: Path to mem0 config.json (for applying the fix)
|
|
144
|
+
verbose: Print detailed output
|
|
145
|
+
n_facts: Number of test facts per run
|
|
146
|
+
|
|
147
|
+
Returns dict with: initial_score, improved_score, improvement_pp, applied, prompt, status
|
|
148
|
+
"""
|
|
149
|
+
result = {
|
|
150
|
+
"success": False,
|
|
151
|
+
"initial_score": None,
|
|
152
|
+
"initial_pct": None,
|
|
153
|
+
"improved_score": None,
|
|
154
|
+
"improved_pct": None,
|
|
155
|
+
"improvement_pp": None,
|
|
156
|
+
"prompt": None,
|
|
157
|
+
"applied": False,
|
|
158
|
+
"backup_path": None,
|
|
159
|
+
"verdict": None,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
facts = generate_test_facts_for_categories(["technical", "research"], count=n_facts)
|
|
163
|
+
uid_baseline = "zer0lint_baseline"
|
|
164
|
+
uid_improved = "zer0lint_improved"
|
|
165
|
+
|
|
166
|
+
# --- Phase 1: Baseline (current config, no changes) ---
|
|
167
|
+
if verbose:
|
|
168
|
+
print("\n[1/3] Baseline — testing current config as-is...")
|
|
169
|
+
|
|
170
|
+
mem_baseline = _make_memory(base_config, collection_suffix="baseline")
|
|
171
|
+
cleanup_test_memories(mem_baseline, user_id=uid_baseline)
|
|
172
|
+
res_baseline = validate_extraction_prompt(mem_baseline, facts, "", user_id=uid_baseline, wait_seconds=1.5)
|
|
173
|
+
|
|
174
|
+
initial_score = res_baseline["score"]
|
|
175
|
+
initial_pct = initial_score / n_facts * 100
|
|
176
|
+
result["initial_score"] = initial_score
|
|
177
|
+
result["initial_pct"] = initial_pct
|
|
178
|
+
|
|
179
|
+
if verbose:
|
|
180
|
+
print(f" Baseline score: {initial_score}/{n_facts} ({initial_pct:.0f}%)")
|
|
181
|
+
for d in res_baseline["details"]:
|
|
182
|
+
icon = "✅" if d["found"] else "❌"
|
|
183
|
+
print(f" {icon} {d['label']}")
|
|
184
|
+
|
|
185
|
+
# If already perfect, done
|
|
186
|
+
if initial_score >= n_facts:
|
|
187
|
+
if verbose:
|
|
188
|
+
print("\n✅ Extraction is already perfect. No changes needed.")
|
|
189
|
+
result["success"] = True
|
|
190
|
+
result["verdict"] = "already_healthy"
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
# --- Phase 2: Re-test with zer0lint technical prompt (config-level) ---
|
|
194
|
+
if verbose:
|
|
195
|
+
print("\n[2/3] Re-testing with zer0lint technical extraction prompt (config-level)...")
|
|
196
|
+
|
|
197
|
+
mem_improved = _make_memory(base_config, custom_prompt=TECHNICAL_EXTRACTION_PROMPT, collection_suffix="improved")
|
|
198
|
+
cleanup_test_memories(mem_improved, user_id=uid_improved)
|
|
199
|
+
res_improved = validate_extraction_prompt(mem_improved, facts, "", user_id=uid_improved, wait_seconds=1.5)
|
|
200
|
+
|
|
201
|
+
improved_score = res_improved["score"]
|
|
202
|
+
improved_pct = improved_score / n_facts * 100
|
|
203
|
+
improvement_pp = improved_pct - initial_pct
|
|
204
|
+
result["improved_score"] = improved_score
|
|
205
|
+
result["improved_pct"] = improved_pct
|
|
206
|
+
result["improvement_pp"] = improvement_pp
|
|
207
|
+
result["prompt"] = TECHNICAL_EXTRACTION_PROMPT
|
|
208
|
+
|
|
209
|
+
if verbose:
|
|
210
|
+
print(f" Improved score: {improved_score}/{n_facts} ({improved_pct:.0f}%)")
|
|
211
|
+
print(f" Improvement: {improvement_pp:+.0f}pp")
|
|
212
|
+
for d in res_improved["details"]:
|
|
213
|
+
icon = "✅" if d["found"] else "❌"
|
|
214
|
+
print(f" {icon} {d['label']}")
|
|
215
|
+
|
|
216
|
+
# --- Phase 3: Apply if improved and above threshold ---
|
|
217
|
+
if improvement_pp > 0 and improved_score >= max(initial_score, 4):
|
|
218
|
+
result["verdict"] = "improved"
|
|
219
|
+
if config_path:
|
|
220
|
+
if verbose:
|
|
221
|
+
print(f"\n[3/3] Applying fix to config ({initial_pct:.0f}% → {improved_pct:.0f}%)...")
|
|
222
|
+
apply_result = apply_prompt(config_path, TECHNICAL_EXTRACTION_PROMPT, backup=True)
|
|
223
|
+
result["applied"] = apply_result["success"]
|
|
224
|
+
result["backup_path"] = apply_result.get("backup_path")
|
|
225
|
+
if verbose:
|
|
226
|
+
print(f" ✅ Config updated. Backup at: {apply_result.get('backup_path')}")
|
|
227
|
+
else:
|
|
228
|
+
if verbose:
|
|
229
|
+
print(f"\n[3/3] Would apply fix but no --config path given (dry run).")
|
|
230
|
+
elif improvement_pp <= 0:
|
|
231
|
+
result["verdict"] = "no_improvement"
|
|
232
|
+
if verbose:
|
|
233
|
+
print(f"\n⚠ Prompt did not improve score — not applying.")
|
|
234
|
+
else:
|
|
235
|
+
result["verdict"] = "below_threshold"
|
|
236
|
+
if verbose:
|
|
237
|
+
print(f"\n⚠ Score improved but still below threshold — not applying.")
|
|
238
|
+
|
|
239
|
+
result["success"] = True
|
|
240
|
+
return result
|