sigx 0.1.1__tar.gz

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.
sigx-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fengrru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sigx-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,549 @@
1
+ Metadata-Version: 2.4
2
+ Name: sigx
3
+ Version: 0.1.1
4
+ Summary: Implicit Feedback Signal Extraction for LLM Alignment
5
+ Author: fengrru
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fengrru/sigx
8
+ Project-URL: Repository, https://github.com/fengrru/sigx
9
+ Project-URL: Issues, https://github.com/fengrru/sigx/issues
10
+ Keywords: llm,alignment,dpo,kto,rlhf,preference-learning
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=1.21.0
27
+ Requires-Dist: scikit-learn>=1.0.0
28
+ Provides-Extra: wildchat
29
+ Requires-Dist: datasets>=2.0.0; extra == "wildchat"
30
+ Provides-Extra: llm
31
+ Requires-Dist: openai>=1.0.0; extra == "llm"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
37
+ Requires-Dist: pyright>=1.0.0; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ <div align="center">
41
+
42
+ [![CI](https://github.com/Fengrru/sigx/actions/workflows/ci.yml/badge.svg)](https://github.com/Fengrru/sigx/actions/workflows/ci.yml)
43
+ [![codecov](https://codecov.io/gh/Fengrru/sigx/branch/main/graph/badge.svg)](https://codecov.io/gh/Fengrru/sigx)
44
+ ![Python](https://img.shields.io/badge/python-3.8%2B-blue?style=flat&logo=python&logoColor=white)
45
+ ![License](https://img.shields.io/badge/license-MIT-green?style=flat)
46
+ ![F1](https://img.shields.io/badge/benchmark-F1%200.83-orange?style=flat)
47
+
48
+ # SigX
49
+
50
+ ### Implicit Feedback Signal Extraction for LLM Alignment
51
+
52
+ **Extract preference signals from conversation logs — no human annotation required.**
53
+
54
+ [SigX Documentation](docs/index.md) | [Quick Start](#quick-start) | [API Reference](docs/api/pipeline.md) | [Contributing](CONTRIBUTING.md)
55
+
56
+ </div>
57
+
58
+ ---
59
+
60
+ ## Why SigX?
61
+
62
+ Traditional LLM alignment relies on **expensive human annotations** or **synthetic preferences from GPT-4**. But every production chat log already contains rich preference signals:
63
+
64
+ | What Users Do | What It Means | SigX Signal |
65
+ |:---|:---|:---|
66
+ | Re-ask the same question | Previous answer was unhelpful | `rephrase` |
67
+ | Say "Actually I meant..." | Model misunderstood | `correction` |
68
+ | Say "That's wrong" | Explicit dissatisfaction | `negative` |
69
+ | Say "Thanks! Perfect!" | Explicit satisfaction | `positive` |
70
+ | Say "Never mind" and leave | User gave up | `abandon` |
71
+
72
+ **SigX mines these signals at zero marginal cost** — turning your existing chat logs into DPO/KTO training data.
73
+
74
+ > Inspired by [WildFeedback](https://arxiv.org/abs/2408.15549) (Microsoft Research, 2024) and the broader literature on learning from implicit preferences.
75
+
76
+ ---
77
+
78
+ ## Comparison with Related Tools
79
+
80
+ | Feature | **SigX** | TRL (HuggingFace) | VeRL |
81
+ |:---|:---:|:---:|:---:|
82
+ | **Extract signals from raw logs** | **Yes** | No (expects labeled pairs) | No |
83
+ | **Implicit feedback detection** | **Yes** | No | No |
84
+ | **DPO/KTO output format** | **Yes** | Yes (training only) | Yes (training only) |
85
+ | **Auto-infer chosen responses** | **Yes** | No | No |
86
+ | **Pluggable extractors** | **Yes** | No | No |
87
+ | **Built-in evaluation metrics** | **Yes** | No | No |
88
+ | **Zero GPU required** | **Yes** | No | No |
89
+
90
+ > SigX is not a training framework — it sits **upstream** of TRL/VeRL, producing the preference data they consume.
91
+
92
+ ---
93
+
94
+ ## Highlights
95
+
96
+ <table>
97
+ <tr>
98
+ <td width="50%">
99
+
100
+ ### Core Capabilities
101
+
102
+ - **Pluggable Extractors** — Mix regex, TF-IDF, ML, and LLM detectors
103
+ - **Smart DPO Conversion** — Auto-infer `chosen` from subsequent positive turns
104
+ - **Multi-Format Output** — DPO, KTO, rejection sampling for TRL/VeRL
105
+ - **Built-in Evaluation** — Per-type precision/recall/F1 against benchmarks
106
+
107
+ </td>
108
+ <td width="50%">
109
+
110
+ ### Design Principles
111
+
112
+ - **Lightweight** — Only `numpy` + `scikit-learn`. No GPU needed.
113
+ - **Extensible** — Subclass `BaseExtractor` for custom detectors
114
+ - **Production-ready** — Type annotations, 92 tests, CI/CD
115
+ - **Framework-agnostic** — Output compatible with TRL, VeRL, and more
116
+
117
+ </td>
118
+ </tr>
119
+ </table>
120
+
121
+ ---
122
+
123
+ ## Quick Start
124
+
125
+ ```bash
126
+ pip install git+https://github.com/Fengrru/sigx.git
127
+ ```
128
+
129
+ ### 1. Extract Signals from Conversations
130
+
131
+ ```python
132
+ from sigx import Pipeline, RephraseDetector, SentimentDetector
133
+
134
+ pipeline = Pipeline([
135
+ RephraseDetector(similarity_threshold=0.6),
136
+ SentimentDetector(min_confidence=0.6),
137
+ ])
138
+
139
+ conversations = [
140
+ {
141
+ "conversation_id": "1",
142
+ "conversation": [
143
+ {"role": "user", "content": "What is Python?"},
144
+ {"role": "assistant", "content": "Python is a type of snake."},
145
+ {"role": "user", "content": "That's not what I asked. I meant the programming language."},
146
+ {"role": "assistant", "content": "Python is a high-level programming language created by Guido van Rossum."},
147
+ {"role": "user", "content": "Thanks! That's exactly what I needed."},
148
+ ]
149
+ }
150
+ ]
151
+
152
+ signals = pipeline.run(conversations)
153
+ for s in signals:
154
+ print(f"[{s.signal_type}] confidence={s.confidence:.2f} | {s.evidence[:60]}...")
155
+ ```
156
+
157
+ **Output:**
158
+ ```
159
+ [negative] confidence=0.95 | That's not what I asked. I meant the programming language....
160
+ [positive] confidence=0.80 | Thanks! That's exactly what I needed....
161
+ ```
162
+
163
+ ### 2. Convert to DPO Training Pairs
164
+
165
+ ```python
166
+ pairs = pipeline.to_dpo(conversations)
167
+ for p in pairs:
168
+ print(f"Prompt: {p.prompt[:80]}...")
169
+ print(f"Rejected: {p.rejected[:80]}...")
170
+ print(f"Chosen: {p.chosen[:80] if p.chosen else 'None'}...")
171
+ ```
172
+
173
+ **Output:**
174
+ ```
175
+ Prompt: User: What is Python?...
176
+ Rejected: Python is a type of snake....
177
+ Chosen: Python is a high-level programming language created by Guido van Rossum....
178
+ ```
179
+
180
+ ### 3. Feed to TRL for Training
181
+
182
+ ```python
183
+ from trl import DPOTrainer
184
+
185
+ trainer = DPOTrainer(
186
+ model=model,
187
+ train_dataset=pairs, # SigX output works directly with TRL
188
+ )
189
+ trainer.train()
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Installation
195
+
196
+ > PyPI release coming soon. For now, install from source:
197
+
198
+ | Install Command | Includes |
199
+ |:---|:---|
200
+ | `pip install git+https://github.com/Fengrru/sigx.git` | Core (numpy, scikit-learn) |
201
+ | `pip install "sigx[llm] @ git+https://github.com/Fengrru/sigx.git"` | + OpenAI API for LLMExtractor |
202
+ | `pip install "sigx[wildchat] @ git+https://github.com/Fengrru/sigx.git"` | + HuggingFace datasets for WildChat |
203
+ | `git clone https://github.com/Fengrru/sigx.git && pip install -e ".[dev]"` | + pytest, ruff, mypy, pyright |
204
+
205
+ ---
206
+
207
+ ## Signal Types
208
+
209
+ SigX detects five categories of implicit feedback:
210
+
211
+ | Signal | Trigger Pattern | Confidence Range | Training Implication |
212
+ |:---|:---|:---:|:---|
213
+ | `rephrase` | User re-asks similar question (TF-IDF cosine sim) | 0.60 – 1.00 | Previous response → `rejected` |
214
+ | `correction` | "Actually I meant...", "No, that's not..." | 0.60 – 0.95 | Model output → `rejected` |
215
+ | `negative` | "That's wrong", "Not helpful" | 0.65 – 0.95 | Explicit dissatisfaction → `rejected` |
216
+ | `positive` | "Thanks!", "Exactly what I needed" | 0.60 – 0.90 | Explicit satisfaction → `chosen` / `label=True` |
217
+ | `abandon` | "Never mind", "I give up", trailing assistant | 0.35 – 0.90 | User gave up → `rejected` |
218
+
219
+ ---
220
+
221
+ ## Extractors
222
+
223
+ ### RephraseDetector
224
+
225
+ Detects when users rephrase or repeat a question using TF-IDF cosine similarity.
226
+ For CJK text (Chinese/Japanese/Korean) it automatically switches to character
227
+ n-grams, since whitespace tokenization does not apply to those languages.
228
+
229
+ ```python
230
+ from sigx import RephraseDetector
231
+
232
+ detector = RephraseDetector(
233
+ similarity_threshold=0.6, # Cosine similarity threshold
234
+ min_turn_length=20, # Skip user turns shorter than this (chars)
235
+ skip_acknowledgments=True, # Skip "thanks", "ok", etc.
236
+ )
237
+ ```
238
+
239
+ ### SentimentDetector
240
+
241
+ Hybrid regex + ML detection with 41 built-in patterns for corrections, negatives, positives, and sarcasm.
242
+
243
+ ```python
244
+ from sigx import SentimentDetector
245
+
246
+ # Rule-only mode (default, no extra deps)
247
+ detector = SentimentDetector(min_confidence=0.6)
248
+
249
+ # ML-enhanced mode (broader coverage)
250
+ detector = SentimentDetector(use_ml=True, min_confidence=0.5)
251
+ detector.fit(
252
+ texts=["that's wrong", "thanks!", "actually I meant..."],
253
+ labels=["negative", "positive", "correction"],
254
+ )
255
+ ```
256
+
257
+ ### AbandonDetector
258
+
259
+ Detects when users give up via explicit patterns or conversation structure analysis.
260
+
261
+ ```python
262
+ from sigx import AbandonDetector
263
+
264
+ detector = AbandonDetector(
265
+ min_assistant_length=300, # Min chars for trailing-assistant
266
+ min_turns=3, # Min conversation length
267
+ require_unanswered_question=True, # Only flag if user asked a question
268
+ )
269
+ ```
270
+
271
+ ### LLMExtractor
272
+
273
+ High-accuracy classification using any OpenAI-compatible LLM.
274
+
275
+ ```python
276
+ from sigx import LLMExtractor
277
+
278
+ detector = LLMExtractor(
279
+ model="gpt-4o-mini", # Or "qwen2.5-7b-instruct" via vLLM
280
+ base_url=None, # Defaults to OpenAI; set for Ollama/vLLM
281
+ api_key="sk-...", # Or set OPENAI_API_KEY env var
282
+ min_confidence=0.6,
283
+ )
284
+ ```
285
+
286
+ ### Custom Extractors
287
+
288
+ ```python
289
+ from sigx.extractors import BaseExtractor
290
+ from sigx.types import Signal
291
+
292
+ class ToxicityExtractor(BaseExtractor):
293
+ name = "toxicity"
294
+
295
+ def extract(self, conversation: dict) -> list[Signal]:
296
+ signals = []
297
+ # Your detection logic here
298
+ return signals
299
+
300
+ pipeline = Pipeline([ToxicityExtractor()])
301
+ ```
302
+
303
+ ---
304
+
305
+ ## DPO Chosen Strategies
306
+
307
+ A key innovation: SigX automatically infers the `chosen` response from subsequent conversation turns.
308
+
309
+ | Strategy | Behavior | Use Case |
310
+ |:---|:---|:---|
311
+ | `subsequent` *(default)* | Find positive turn after negative signal; use assistant response before it as `chosen` | Best quality |
312
+ | `last_assistant` | Use the final assistant response as `chosen` | Simple fallback |
313
+ | `none` | `chosen=None` | Backward compatible |
314
+
315
+ ```python
316
+ from sigx import Pipeline, CHOSEN_SUBSEQUENT, CHOSEN_NONE
317
+
318
+ # Default: smart chosen inference
319
+ pipeline = Pipeline(extractors=[...], chosen_strategy=CHOSEN_SUBSEQUENT)
320
+
321
+ # Backward compatible: no chosen inference
322
+ pipeline = Pipeline(extractors=[...], chosen_strategy=CHOSEN_NONE)
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Data Loading
328
+
329
+ SigX supports multiple conversation formats:
330
+
331
+ ```python
332
+ from sigx import load_conversations, load_wildchat, stream_wildchat
333
+
334
+ # ShareGPT format
335
+ convos = load_conversations("sharegpt_data.json", format="sharegpt")
336
+
337
+ # OpenAI chat format
338
+ convos = load_conversations("openai_data.json", format="openai")
339
+
340
+ # Generic JSONL
341
+ convos = load_conversations("logs.jsonl", format="jsonl")
342
+
343
+ # WildChat from HuggingFace (1M+ conversations)
344
+ convos = load_wildchat(n=1000)
345
+
346
+ # Stream large datasets (memory efficient)
347
+ for convo in stream_wildchat(n=10000):
348
+ signals = pipeline.run([convo])
349
+ ```
350
+
351
+ ---
352
+
353
+ ## Output Formats
354
+
355
+ ### DPO (Direct Preference Optimization)
356
+
357
+ ```python
358
+ pairs = pipeline.to_dpo(conversations)
359
+ # List[PreferencePair] — compatible with TRL's DPOTrainer
360
+ ```
361
+
362
+ ### KTO (Kahneman-Tversky Optimization)
363
+
364
+ ```python
365
+ examples = pipeline.to_kto(conversations)
366
+ # List[KTOExample] — binary labels: True (desirable), False (undesirable)
367
+ ```
368
+
369
+ ### Rejection Sampling
370
+
371
+ ```python
372
+ pairs = pipeline.to_rejection(conversations)
373
+ # List[dict] with keys: prompt, rejected, signal_type, confidence
374
+ ```
375
+
376
+ ---
377
+
378
+ ## End-to-End Training Pipeline
379
+
380
+ ```mermaid
381
+ %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#e3f2fd', 'primaryTextColor': '#1565c0', 'primaryBorderColor': '#1565c0', 'lineColor': '#90a4ae', 'secondaryColor': '#fff3e0', 'tertiaryColor': '#e8f5e9'}}}%%
382
+
383
+ graph LR
384
+ subgraph INPUT [" "]
385
+ direction TB
386
+ I1["JSONL"]
387
+ I2["ShareGPT"]
388
+ I3["WildChat"]
389
+ I4["OpenAI"]
390
+ end
391
+
392
+ subgraph SIGX ["SigX"]
393
+ direction LR
394
+ S1["Extract"] --> S2["Filter"] --> S3["Convert"]
395
+ end
396
+
397
+ subgraph OUTPUT [" "]
398
+ direction TB
399
+ O1["DPO Pairs"]
400
+ O2["KTO Examples"]
401
+ end
402
+
403
+ INPUT --> SIGX --> OUTPUT
404
+
405
+ style INPUT fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1
406
+ style SIGX fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#bf360c
407
+ style OUTPUT fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
408
+ ```
409
+
410
+ ```python
411
+ from sigx import Pipeline, SentimentDetector, RephraseDetector, AbandonDetector
412
+ from sigx.io import load_wildchat
413
+
414
+ # 1. Configure pipeline
415
+ pipeline = Pipeline([
416
+ SentimentDetector(min_confidence=0.6),
417
+ RephraseDetector(similarity_threshold=0.6),
418
+ AbandonDetector(min_turns=3),
419
+ ])
420
+
421
+ # 2. Load conversation data
422
+ convos = load_wildchat(n=10000)
423
+
424
+ # 3. Convert to DPO training pairs
425
+ dpo_pairs = pipeline.to_dpo(convos)
426
+
427
+ # 4. Feed directly to TRL
428
+ from trl import DPOTrainer
429
+ trainer = DPOTrainer(model=model, train_dataset=dpo_pairs)
430
+ trainer.train()
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Evaluation
436
+
437
+ Evaluate extraction quality against a labeled benchmark:
438
+
439
+ ```python
440
+ metrics = pipeline.evaluate("benchmark.json")
441
+
442
+ print(f"Overall F1: {metrics['overall']['f1']:.4f}")
443
+ # Per-type breakdown
444
+ for signal_type, m in metrics["per_type"].items():
445
+ print(f" {signal_type}: P={m['precision']:.4f} R={m['recall']:.4f} F1={m['f1']:.4f}")
446
+ ```
447
+
448
+ ### Benchmark Results
449
+
450
+ Evaluated on a 20-conversation benchmark with regex-based extractors (default settings, no LLM):
451
+
452
+ ![Benchmark results](docs/assets/benchmark.png)
453
+
454
+ > Reproduce with `pipeline.evaluate("tests/benchmark.json")`; regenerate the chart with `python scripts/generate_assets.py`. Adding `LLMExtractor` significantly improves recall on `negative` and `rephrase` types.
455
+
456
+ ---
457
+
458
+ ## Architecture
459
+
460
+ ```
461
+ sigx/
462
+ ├── pipeline.py # Orchestration: extract → filter → convert
463
+ ├── types.py # Core dataclasses: Signal, PreferencePair, KTOExample
464
+ ├── exceptions.py # Custom exception hierarchy
465
+ ├── extractors/
466
+ │ ├── base.py # Abstract BaseExtractor
467
+ │ ├── rephrase.py # TF-IDF cosine similarity rephrase detection
468
+ │ ├── sentiment.py # Regex + optional ML sentiment classifier
469
+ │ ├── abandon.py # Frustration pattern + trailing-assistant detection
470
+ │ └── llm.py # OpenAI-compatible LLM classifier
471
+ ├── filters/
472
+ │ └── quality.py # Confidence threshold, dedup, per-conv limits
473
+ ├── converters/
474
+ │ └── preference.py # DPO / KTO / rejection-sampling converters
475
+ └── io/
476
+ └── loader.py # ShareGPT, OpenAI, WildChat, JSONL loaders
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Research Background
482
+
483
+ SigX is built on growing evidence that **implicit feedback from real user interactions produces better alignment data** than synthetic alternatives:
484
+
485
+ | Paper | Institution | Year | Key Finding |
486
+ |:---|:---|:---:|:---|
487
+ | [WildFeedback](https://arxiv.org/abs/2408.15549) | Microsoft Research | 2024 | 20K preference pairs from 148K ChatGPT conversations outperformed UltraFeedback on AlpacaEval 2, Arena-Hard, MT-Bench |
488
+
489
+ **The key insight**: Real user feedback captures nuance that synthetic data misses, and the signals are already present in existing chat logs — they just need to be extracted.
490
+
491
+ ### How SigX Relates
492
+
493
+ | Project | Focus | SigX Connection |
494
+ |:---|:---|:---|
495
+ | [WildFeedback](https://arxiv.org/abs/2408.15549) | Extracting preference pairs from in-situ feedback | SigX implements the extraction pipeline in Python |
496
+ | [TRL](https://github.com/huggingface/trl) | Post-training (DPO/KTO/PPO) | SigX output is TRL-compatible |
497
+ | [VeRL](https://github.com/volcengine/verl) | RLHF framework | SigX output works with VeRL |
498
+
499
+ ---
500
+
501
+ ## Development
502
+
503
+ ```bash
504
+ git clone https://github.com/fengrru/sigx.git
505
+ cd sigx
506
+ pip install -e ".[dev]"
507
+
508
+ # Run tests
509
+ pytest # 92 tests
510
+ pytest --cov=sigx # With coverage
511
+
512
+ # Lint
513
+ ruff check . # Lint
514
+ ruff format . # Format
515
+
516
+ # Type check
517
+ mypy sigx/ # mypy
518
+ pyright # pyright
519
+ ```
520
+
521
+ ---
522
+
523
+ ## Citation
524
+
525
+ ```bibtex
526
+ @software{sigx2026,
527
+ title = {{SigX: Implicit Feedback Signal Extraction for LLM Alignment}},
528
+ author = {fengrru},
529
+ license = {MIT},
530
+ url = {https://github.com/fengrru/sigx},
531
+ year = {2026},
532
+ }
533
+ ```
534
+
535
+ ---
536
+
537
+ ## License
538
+
539
+ MIT License — see [LICENSE](LICENSE) for details.
540
+
541
+ ---
542
+
543
+ <div align="center">
544
+
545
+ **Built with care for the LLM alignment community.**
546
+
547
+ [Report Bug](https://github.com/fengrru/sigx/issues) · [Request Feature](https://github.com/fengrru/sigx/issues) · [Contributing Guide](CONTRIBUTING.md)
548
+
549
+ </div>