-
Notifications
You must be signed in to change notification settings - Fork 0
Development
signal/
├── main.py # Entry point + venv bootstrap + index/archive generation
├── config/sources.yaml # All runtime configuration
├── pipeline/
│ ├── __init__.py # Initialization file
│ ├── collector.py # Feed ingestion, full-text extraction
│ ├── analyzer.py # Passes 1–5 + LLM dispatch
│ ├── prompts.py # All LLM prompt templates
│ ├── reporter.py # HTML report rendering
│ └── store.py # SQLite persistence
├── scripts/
│ ├── run_and_publish.sh # Automation wrapper
│ └── com.flexrpl.signal.plist # launchd configuration file
├── docs/ # Operational docs
├── reports/ # Generated HTML reports (committed)
├── logs/ # Runtime logs (gitignored)
└── images/ # Static assetsgit clone https://github.com/fleXRPL/signal.git
cd signal
# Dependencies install automatically on first run, OR manually:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt| Package | Version | Purpose |
|---|---|---|
feedparser |
≥6.0.11 | RSS/Atom feed parsing |
httpx |
≥0.27.0 | Async-capable HTTP client for full-text fetching |
beautifulsoup4 |
≥4.12.3 | HTML parsing for article body extraction |
lxml |
≥5.2.0 | Fast HTML/XML parser backend for BeautifulSoup |
python-dateutil |
≥2.9.0 | Robust date parsing from feed entries |
rapidfuzz |
≥3.9.0 | Fast fuzzy string matching for title clustering |
rich |
≥13.7.0 | Terminal progress bars and colored output |
pyyaml |
≥6.0.1 | YAML config file parsing |
ollama |
≥0.3.3 | Ollama Python client (used when provider=ollama) |
jinja2 |
≥3.1.4 | Available but not currently used in templates |
The project targets SonarQube Cloud quality gates. All code must pass:
- No critical or major SonarQube issues
- No security hotspots
- Line length ≤ 120 characters
- Docstrings on all public functions
- Type hints on all function signatures
SonarQube scanning runs automatically on push via the SonarLint VSCode extension. The SonarQube tab in Cursor/VS Code shows live issues.
Error handling in LLM calls:
try:
raw = _llm_call(prompt, model, base_url, timeout, provider)
parsed = _parse_json_response(raw)
except Exception as exc: # noqa: BLE001
console.print(f" [red]LLM error:[/red] {exc}")
parsed = NoneAll LLM errors are caught, logged, and handled gracefully. The pipeline never aborts due to a single failed LLM call.
JSON parsing:
Always use _parse_json_response() rather than json.loads() directly on LLM output. It handles markdown fences, leading text, and embedded JSON.
If you want to add a sixth pass (e.g. sentiment trend tracking):
-
Add a prompt template in
pipeline/prompts.py:SENTIMENT_TREND = """\ You are an analyst tracking sentiment trends... Return ONLY a valid JSON object: {{ ... }}"""
-
Add a function in
pipeline/analyzer.py:def track_sentiment_trends( articles: List[Dict[str, Any]], run_id: int, model: str, base_url: str, timeout: int, provider: str = "claude", ) -> Dict[str, Any]: """Pass 6: Track sentiment trends across sources.""" ...
-
Wire it into
run_pipeline()inpipeline/analyzer.py:# Pass 6 trends = track_sentiment_trends(articles, run_id, model, base_url, timeout, provider) return brief, clusters, correlation, trends
-
Update
run_signal()inmain.pyto accept and use the new return value. -
Add a DB table in
store.pyif persistence is needed. -
Add a rendering function in
reporter.pyto display the output in the HTML report.
The report template is HTML_TEMPLATE in pipeline/reporter.py — a large multi-line string with Python .format() placeholders. It uses inline CSS (GitHub-dark theme) with CSS custom properties.
Key rendering functions:
| Function | Output |
|---|---|
_render_brief_sections() |
Parses markdown → HTML for the 6 brief sections |
_render_story_cards() |
Story cluster cards with framing analysis |
_render_connections() |
Hidden connections list |
_render_list_items() |
Generic list renderer for patterns/anomalies/watch items |
_render_side_coverage() |
Left-only/right-only blindspot columns |
_render_source_index() |
Collapsible source index |
index.html and archive.html are generated inline by _update_index() in main.py. There is no separate template file — the HTML is an f-string in the function body. This was a deliberate choice to keep the pipeline self-contained in a single repository with no build step.
There is no formal test suite currently. The recommended testing workflow:
# 1. Verify feed connectivity (no LLM, instant)
python3 main.py --collect-only
# 2. Fast end-to-end test with Claude (uses RSS snippets)
python3 main.py --no-fetch
# 3. Full production run
python3 main.py
# 4. Test the full automation script
bash scripts/run_and_publish.sh
tail -f logs/cron.logA formal pytest suite is planned. See Future Roadmap.
| Variable | Values | Default | Effect |
|---|---|---|---|
SIGNAL_LLM_PROVIDER |
claude | ollama
|
claude (via sources.yaml) |
Selects the LLM backend |
The main branch is the production branch. Every push to main triggers GitHub Actions to redeploy GitHub Pages.
Reports and HTML pages are committed directly to main by run_and_publish.sh. There is no separate gh-pages branch — the site is served from the repo root on main.
Never force-push to main — it would rewrite the report history visible on the archive page.
Signal · Repository · fleXRPL · Daily political intelligence — powered by local AI