-
Notifications
You must be signed in to change notification settings - Fork 7
feat: unified enrichment controller — brain_enrich MCP tool + 3 backends #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| #!/usr/bin/env bash | ||
| # backfill_orchestrate.sh — Full pipeline orchestrator for Gemini batch enrichment | ||
| # Handles: resume submitted → fix broken-completed → resubmit failed+unsubmitted → resume again | ||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| PYTHON="$SCRIPT_DIR/../.venv/bin/python3" | ||
| BACKFILL="$SCRIPT_DIR/cloud_backfill.py" | ||
| LOG="/tmp/backfill_orchestrate.log" | ||
| API_KEY="${GOOGLE_API_KEY:-}" | ||
|
|
||
| if [[ -z "$API_KEY" ]]; then | ||
| echo "ERROR: GOOGLE_API_KEY not set" | ||
| exit 1 | ||
| fi | ||
|
|
||
| log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; } | ||
|
|
||
| log "=== ORCHESTRATOR START ===" | ||
| cd "$SCRIPT_DIR/.." | ||
|
|
||
| # PHASE 1: Resume all submitted batches (may already be running) | ||
| log "Phase 1: Resume submitted batches..." | ||
| GOOGLE_API_KEY="$API_KEY" "$PYTHON" -u "$BACKFILL" --resume 2>&1 | tee -a "$LOG" | ||
|
|
||
| # PHASE 2: Fix "completed" batches that had 0 imports (broken download bug) | ||
| # The broken run completed all its batches at 2026-03-14T12:21 UTC. | ||
| # Current (correct) run completes batches after 2026-03-14T12:22 UTC. | ||
| log "" | ||
| log "Phase 2: Checking for incorrectly-completed batches (0 imports due to broken download)..." | ||
| "$PYTHON" -u - <<'PYEOF' 2>&1 | tee -a "$LOG" | ||
| import apsw | ||
| from pathlib import Path | ||
|
|
||
| cp_db = Path.home() / '.local/share/brainlayer/enrichment_checkpoints.db' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low Phase 2's inline Python hardcodes the checkpoint database path to 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| conn = apsw.Connection(str(cp_db)) | ||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded database path violates coding guidelines. Line 35 hardcodes the path 🐛 Proposed fix to use paths.py "$PYTHON" -u - <<'PYEOF' 2>&1 | tee -a "$LOG"
import apsw
from pathlib import Path
+import sys
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+from brainlayer.paths import get_db_path
-cp_db = Path.home() / '.local/share/brainlayer/enrichment_checkpoints.db'
+# enrichment_checkpoints.db is in the same directory as the main DB
+cp_db = get_db_path().parent / 'enrichment_checkpoints.db'
conn = apsw.Connection(str(cp_db))Note: The inline Python heredoc may need adjustment since 🤖 Prompt for AI Agents |
||
| conn.setbusytimeout(10000) | ||
|
|
||
| # Find completed batches from the broken 3rd run (completed at 12:21 UTC) | ||
| # These had 0 imports due to Files.download(name=) bug that has since been fixed | ||
| rows = list(conn.cursor().execute(""" | ||
| SELECT batch_id, chunk_count, completed_at | ||
| FROM enrichment_checkpoints | ||
| WHERE status = 'completed' | ||
| AND completed_at < '2026-03-14T12:22:00' | ||
| """)) | ||
|
|
||
| print(f"Found {len(rows)} broken 'completed' batches (before fix at 12:22 UTC)") | ||
| if rows: | ||
| # Reset them to 'submitted' so --resume will re-process them | ||
| conn.cursor().execute(""" | ||
| UPDATE enrichment_checkpoints | ||
| SET status = 'submitted', completed_at = NULL | ||
| WHERE status = 'completed' | ||
| AND completed_at < '2026-03-14T12:22:00' | ||
| """) | ||
| print(f"Reset {len(rows)} batches to 'submitted'") | ||
| else: | ||
| print("No broken batches found - all completed batches imported correctly") | ||
|
|
||
| conn.close() | ||
| PYEOF | ||
|
Comment on lines
+26
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Phase 2 contains one-time fix logic with hardcoded timestamp. This phase resets batches completed before
Would you like me to create a separate one-time migration script that can be removed after execution? 🤖 Prompt for AI Agents |
||
|
|
||
| # PHASE 3: Resume the reset batches | ||
| log "" | ||
| log "Phase 3: Resume reset batches..." | ||
| GOOGLE_API_KEY="$API_KEY" "$PYTHON" -u "$BACKFILL" --resume 2>&1 | tee -a "$LOG" | ||
|
|
||
| # PHASE 4: Resubmit failed batches + submit unsubmitted JSONL files | ||
| log "" | ||
| log "Phase 4: Resubmit failed/unsubmitted batches..." | ||
| GOOGLE_API_KEY="$API_KEY" "$PYTHON" -u "$BACKFILL" --submit-only 2>&1 | tee -a "$LOG" | ||
|
|
||
| log "" | ||
| log "Phase 4 done. Waiting 60s for jobs to register before polling..." | ||
| sleep 60 | ||
|
|
||
| # PHASE 5: Resume all newly submitted batches | ||
| log "" | ||
| log "Phase 5: Resume newly submitted batches (poll + import)..." | ||
| GOOGLE_API_KEY="$API_KEY" "$PYTHON" -u "$BACKFILL" --resume 2>&1 | tee -a "$LOG" | ||
|
|
||
| # Final stats | ||
| log "" | ||
| log "=== FINAL STATUS ===" | ||
| GOOGLE_API_KEY="$API_KEY" "$PYTHON" -u "$BACKFILL" --status 2>&1 | tee -a "$LOG" | ||
|
|
||
| log "=== ORCHESTRATOR COMPLETE ===" | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||
| """Paced batch submission — submits one batch at a time with delays to avoid 429s. | ||||||||||||||||||||||||||||||||||
| Bypasses VectorStore entirely to avoid DB lock issues with BrainBar. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Usage: GOOGLE_API_KEY=... python3 scripts/batch_submit_paced.py [--delay 45] [--max-retries 5] | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| import glob, json, os, sys, time | ||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| import google.generativeai as genai | ||||||||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||||||||
| print("pip install google-generativeai"); sys.exit(1) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| API_KEY = os.environ.get("GOOGLE_API_KEY") | ||||||||||||||||||||||||||||||||||
| if not API_KEY: | ||||||||||||||||||||||||||||||||||
| print("ERROR: GOOGLE_API_KEY required"); sys.exit(1) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| genai.configure(api_key=API_KEY) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| DELAY = int(sys.argv[sys.argv.index("--delay") + 1]) if "--delay" in sys.argv else 45 | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low If 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||||||||||||||||||||||||||||||||||
| MAX_RETRIES = int(sys.argv[sys.argv.index("--max-retries") + 1]) if "--max-retries" in sys.argv else 10 | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CLI argument parsing lacks bounds checking. If ♻️ Proposed fix using argparse+import argparse
import glob, json, os, sys, time
from pathlib import Path
...
-DELAY = int(sys.argv[sys.argv.index("--delay") + 1]) if "--delay" in sys.argv else 45
-MAX_RETRIES = int(sys.argv[sys.argv.index("--max-retries") + 1]) if "--max-retries" in sys.argv else 10
+parser = argparse.ArgumentParser(description="Paced batch submission for Gemini")
+parser.add_argument("--delay", type=int, default=45, help="Delay between submissions (seconds)")
+parser.add_argument("--max-retries", type=int, default=10, help="Max retries per batch")
+args = parser.parse_args()
+DELAY = args.delay
+MAX_RETRIES = args.max_retries🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| MODEL = "gemini-2.5-flash-lite" | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Model differs from cloud_backfill.py and is not configurable. This script hardcodes 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Track what we've already submitted this run | ||||||||||||||||||||||||||||||||||
| STATE_FILE = Path(__file__).parent / "backfill_data" / ".paced_state.json" | ||||||||||||||||||||||||||||||||||
| submitted = {} | ||||||||||||||||||||||||||||||||||
| if STATE_FILE.exists(): | ||||||||||||||||||||||||||||||||||
| submitted = json.loads(STATE_FILE.read_text()) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low If submitted = {}
if STATE_FILE.exists():
- submitted = json.loads(STATE_FILE.read_text())
+ try:
+ submitted = json.loads(STATE_FILE.read_text())
+ except json.JSONDecodeError:
+ submitted = {}🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
Comment on lines
+25
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if both scripts can see the same batch files and use different tracking
echo "=== batch_submit_paced.py state file ==="
cat scripts/backfill_data/.paced_state.json 2>/dev/null || echo "State file not found"
echo ""
echo "=== cloud_backfill.py checkpoint tracking ==="
rg -n "enrichment_checkpoints" scripts/cloud_backfill.py | head -20
echo ""
echo "=== Both scripts glob batch_*.jsonl ==="
rg -n "batch_\*\.jsonl" scripts/Repository: EtanHey/brainlayer Length of output: 801 🏁 Script executed: #!/bin/bash
echo "=== EXPORT_DIR in cloud_backfill.py ==="
rg -n "EXPORT_DIR\s*=" scripts/cloud_backfill.py | head -5
echo ""
echo "=== EXPORT_DIR in cloud_stream.py ==="
rg -n "EXPORT_DIR\s*=" scripts/cloud_stream.py | head -5
echo ""
echo "=== Full context around batch file discovery in batch_submit_paced.py ==="
sed -n '30,35p' scripts/batch_submit_paced.py
echo ""
echo "=== Full context around batch file discovery in cloud_backfill.py ==="
sed -n '260,270p' scripts/cloud_backfill.py
echo ""
echo "=== Check for any lock files or coordination mechanisms ==="
rg -n "\.lock|flock|fcntl" scripts/ | grep -E "(batch_submit_paced|cloud_backfill)" || echo "No lock mechanisms found"Repository: EtanHey/brainlayer Length of output: 1238 🏁 Script executed: #!/bin/bash
echo "=== What does batch_submit_paced.py do with batch files? (full submission logic) ==="
sed -n '40,98p' scripts/batch_submit_paced.py
echo ""
echo "=== What does cloud_backfill.py do in --submit-only mode? ==="
rg -n "submit.only|submit_only" scripts/cloud_backfill.py -A 3 -B 1 | head -40
echo ""
echo "=== Does batch_submit_paced.py reference enrichment_checkpoints.db? ==="
rg -n "enrichment_checkpoints|checkpoint" scripts/batch_submit_paced.py || echo "No checkpoint DB references found"
echo ""
echo "=== Check README or documentation for script usage patterns ==="
head -50 scripts/README.md 2>/dev/null || echo "README not found"Repository: EtanHey/brainlayer Length of output: 3649 🏁 Script executed: #!/bin/bash
echo "=== batch_submit_paced.py header and docstring ==="
head -25 scripts/batch_submit_paced.py
echo ""
echo "=== cloud_backfill.py header/module docstring ==="
head -30 scripts/cloud_backfill.py | grep -A 20 "^#\|^\"\"\"" | head -20
echo ""
echo "=== Check for any usage notes or comments in both scripts ==="
rg -n "Note|WARNING|IMPORTANT|TODO|FIXME" scripts/batch_submit_paced.py scripts/cloud_backfill.py | grep -i "submit\|coordinate\|exclusive\|together"Repository: EtanHey/brainlayer Length of output: 1706 Dual-tracking with cloud_backfill.py creates double-submission risk. Both scripts search Given that
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Find all batch JSONL files | ||||||||||||||||||||||||||||||||||
| batch_files = sorted(glob.glob(str(Path(__file__).parent / "backfill_data" / "batch_*.jsonl"))) | ||||||||||||||||||||||||||||||||||
| print(f"Total batch files: {len(batch_files)}") | ||||||||||||||||||||||||||||||||||
| print(f"Already submitted (this run): {len(submitted)}") | ||||||||||||||||||||||||||||||||||
| print(f"Delay between submissions: {DELAY}s") | ||||||||||||||||||||||||||||||||||
| print(f"Max retries per batch: {MAX_RETRIES}") | ||||||||||||||||||||||||||||||||||
| print(f"Model: {MODEL}") | ||||||||||||||||||||||||||||||||||
| print() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| created = 0 | ||||||||||||||||||||||||||||||||||
| skipped = 0 | ||||||||||||||||||||||||||||||||||
| failed = 0 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for i, fpath in enumerate(batch_files): | ||||||||||||||||||||||||||||||||||
| fname = Path(fpath).name | ||||||||||||||||||||||||||||||||||
| if fname in submitted: | ||||||||||||||||||||||||||||||||||
| skipped += 1 | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Count chunks in this file | ||||||||||||||||||||||||||||||||||
| with open(fpath) as f: | ||||||||||||||||||||||||||||||||||
| chunks = sum(1 for _ in f) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| print(f"[{i+1}/{len(batch_files)}] {fname} ({chunks} chunks)") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Upload file | ||||||||||||||||||||||||||||||||||
| for attempt in range(MAX_RETRIES): | ||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| print(f" Uploading...", end="", flush=True) | ||||||||||||||||||||||||||||||||||
| uploaded = genai.upload_file(fpath) | ||||||||||||||||||||||||||||||||||
| print(f" ok ({uploaded.name})") | ||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||
| wait = min(30 * (attempt + 1), 300) | ||||||||||||||||||||||||||||||||||
| print(f" 429, waiting {wait}s (attempt {attempt+1}/{MAX_RETRIES})") | ||||||||||||||||||||||||||||||||||
| time.sleep(wait) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Upload retry assumes all errors are rate-limit 429s. The exception handler treats every failure as a rate-limit error, printing "429" and retrying. Non-transient errors (authentication, network, malformed request) will be retried unnecessarily with misleading output. 🐛 Proposed fix to differentiate error types except Exception as e:
- wait = min(30 * (attempt + 1), 300)
- print(f" 429, waiting {wait}s (attempt {attempt+1}/{MAX_RETRIES})")
- time.sleep(wait)
+ err_str = str(e)
+ if "429" in err_str or "RESOURCE_EXHAUSTED" in err_str:
+ wait = min(30 * (attempt + 1), 300)
+ print(f" rate-limited, waiting {wait}s (attempt {attempt+1}/{MAX_RETRIES})")
+ time.sleep(wait)
+ else:
+ print(f" ERROR: {e}")
+ failed += 1
+ break📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||
| print(f" FAILED upload after {MAX_RETRIES} retries, skipping") | ||||||||||||||||||||||||||||||||||
| failed += 1 | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Create batch job | ||||||||||||||||||||||||||||||||||
| for attempt in range(MAX_RETRIES): | ||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||
| print(f" Creating batch job...", end="", flush=True) | ||||||||||||||||||||||||||||||||||
| job = genai.batches.create( | ||||||||||||||||||||||||||||||||||
| model=f"models/{MODEL}", | ||||||||||||||||||||||||||||||||||
| src=uploaded.uri, | ||||||||||||||||||||||||||||||||||
| config={"display_name": fname}, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| print(f" ok ({job.name}, {job.state})") | ||||||||||||||||||||||||||||||||||
| submitted[fname] = {"job_name": job.name, "chunks": chunks, "time": time.strftime("%Y-%m-%dT%H:%M:%S")} | ||||||||||||||||||||||||||||||||||
| STATE_FILE.write_text(json.dumps(submitted, indent=2)) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial State file write is not atomic. If the process crashes during ♻️ Proposed atomic write+import tempfile
...
- STATE_FILE.write_text(json.dumps(submitted, indent=2))
+ # Atomic write to prevent corruption on crash
+ tmp = STATE_FILE.with_suffix('.tmp')
+ tmp.write_text(json.dumps(submitted, indent=2))
+ tmp.rename(STATE_FILE)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| created += 1 | ||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||
| if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e): | ||||||||||||||||||||||||||||||||||
| wait = min(60 * (attempt + 1), 600) | ||||||||||||||||||||||||||||||||||
| print(f" 429, waiting {wait}s (attempt {attempt+1}/{MAX_RETRIES})") | ||||||||||||||||||||||||||||||||||
| time.sleep(wait) | ||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||
| print(f" ERROR: {e}") | ||||||||||||||||||||||||||||||||||
| failed += 1 | ||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||
| print(f" FAILED create after {MAX_RETRIES} retries, skipping") | ||||||||||||||||||||||||||||||||||
| failed += 1 | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-rate-limit errors during job creation allow fall-through to delay. When a non-rate-limit error occurs (lines 91-94), the loop breaks but execution continues to the pacing delay (lines 100-103) before moving to the next file. This is inconsistent with the retry exhaustion path (line 98 uses ♻️ Proposed fix for consistent flow else:
print(f" ERROR: {e}")
failed += 1
- break
+ continue
else:
print(f" FAILED create after {MAX_RETRIES} retries, skipping")
failed += 1
continue📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Pace ourselves | ||||||||||||||||||||||||||||||||||
| if i < len(batch_files) - 1: | ||||||||||||||||||||||||||||||||||
| print(f" Waiting {DELAY}s before next...") | ||||||||||||||||||||||||||||||||||
| time.sleep(DELAY) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| print(f"\nDone! Created: {created}, Skipped (already done): {skipped}, Failed: {failed}") | ||||||||||||||||||||||||||||||||||
| print(f"State saved to {STATE_FILE}") | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Label</key> | ||
| <string>com.brainlayer.enrichment</string> | ||
|
|
||
| <!-- Unified enrichment daemon — replaces com.brainlayer.enrich | ||
| Runs realtime enrichment hourly on recent chunks. | ||
| For batch/local modes, use the CLI or brain_enrich MCP tool. --> | ||
|
|
||
| <key>ProgramArguments</key> | ||
| <array> | ||
| <string>__BRAINLAYER_BIN__</string> | ||
| <string>enrich</string> | ||
| <string>--mode</string> | ||
| <string>realtime</string> | ||
| <string>--since-hours</string> | ||
| <string>24</string> | ||
| <string>--limit</string> | ||
| <string>50</string> | ||
| </array> | ||
|
|
||
| <key>StartInterval</key> | ||
| <integer>3600</integer> | ||
|
|
||
| <key>StandardOutPath</key> | ||
| <string>__HOME__/.local/share/brainlayer/logs/enrichment.log</string> | ||
| <key>StandardErrorPath</key> | ||
| <string>__HOME__/.local/share/brainlayer/logs/enrichment.err</string> | ||
|
|
||
| <key>EnvironmentVariables</key> | ||
| <dict> | ||
| <key>PATH</key> | ||
| <string>/usr/local/bin:/usr/bin:/bin:__HOME__/.local/bin</string> | ||
| <key>PYTHONUNBUFFERED</key> | ||
| <string>1</string> | ||
| <key>BRAINLAYER_STALL_TIMEOUT</key> | ||
| <string>300</string> | ||
| <key>GOOGLE_API_KEY</key> | ||
| <string>__GOOGLE_API_KEY__</string> | ||
| <!-- Optional: set regional endpoint for lower latency --> | ||
| <!-- <key>GOOGLE_CLOUD_REGION</key> | ||
| <string>us-central1</string> --> | ||
| </dict> | ||
|
Comment on lines
+32
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding Groq as primary backend per project guidelines. Based on learnings, Groq should be the primary enrichment backend with Gemini as fallback. The current plist only configures Suggested addition to EnvironmentVariables<key>GROQ_API_KEY</key>
<string>__GROQ_API_KEY__</string>
<key>BRAINLAYER_ENRICH_BACKEND</key>
<string>groq</string>Note: The Based on learnings: "Use Groq as primary enrichment backend (configured in launchd plist); fall back to Gemini, then Ollama as offline last-resort" 🤖 Prompt for AI Agents |
||
|
|
||
| <key>RunAtLoad</key> | ||
| <true/> | ||
|
|
||
| <key>Nice</key> | ||
| <integer>15</integer> | ||
|
|
||
| <key>ProcessType</key> | ||
| <string>Background</string> | ||
| </dict> | ||
| </plist> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Hardcoded virtual environment path may not exist.
The script assumes Python is at
../.venv/bin/python3. Consider adding a fallback or validation:♻️ Proposed validation
📝 Committable suggestion
🤖 Prompt for AI Agents