Spaces:
Sleeping
Sleeping
github-actions commited on
Commit Β·
638084e
1
Parent(s): f070f64
Sync from GitHub @ fa87356
Browse files- app.py +197 -1
- knowledge/__init__.py +23 -10
- knowledge/adapter_importer.py +66 -0
- knowledge/dataset_builder.py +53 -0
- knowledge/notebook_generator.py +237 -0
app.py
CHANGED
|
@@ -36,7 +36,11 @@ from pydantic import BaseModel, Field
|
|
| 36 |
# Make the local `knowledge` package importable whether we're running on HF
|
| 37 |
# Spaces (cwd = /app) or locally (cwd = repo root, script in space/).
|
| 38 |
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 39 |
-
from knowledge import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -583,6 +587,127 @@ def handle_delete_all() -> tuple[str, list]:
|
|
| 583 |
return f"ποΈ Cleared {n} document(s).", render_knowledge_table()
|
| 584 |
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
def refresh_all():
|
| 587 |
return (
|
| 588 |
render_active_genome(),
|
|
@@ -702,6 +827,22 @@ with gr.Blocks(title="EvoLLM", theme=gr.themes.Soft(), css=CSS) as demo:
|
|
| 702 |
refresh_btn = gr.Button("π Refresh")
|
| 703 |
evolution_result = gr.Markdown("")
|
| 704 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
# ββ Tab 3: Knowledge βββββββββββββββββββββββββββββββββββββββββ
|
| 706 |
with gr.Tab("π Knowledge"):
|
| 707 |
gr.Markdown(
|
|
@@ -752,6 +893,39 @@ with gr.Blocks(title="EvoLLM", theme=gr.themes.Soft(), css=CSS) as demo:
|
|
| 752 |
clear_knowledge_btn = gr.Button("ποΈ Clear all documents", variant="stop")
|
| 753 |
knowledge_action_status = gr.Markdown("")
|
| 754 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
# ββ Tab 4: Evolution Log βββββββββββββββββββββββββββββββββββββ
|
| 756 |
with gr.Tab("π Evolution Log"):
|
| 757 |
gr.Markdown("### Lineage of mutations, promotions, and feedback events")
|
|
@@ -896,6 +1070,28 @@ Built on EvoTransformer (Mohabeer, 2025).
|
|
| 896 |
api_name=False,
|
| 897 |
).then(render_knowledge_stats, None, knowledge_stats, api_name=False)
|
| 898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
|
| 900 |
if __name__ == "__main__":
|
| 901 |
demo.queue().launch(
|
|
|
|
| 36 |
# Make the local `knowledge` package importable whether we're running on HF
|
| 37 |
# Spaces (cwd = /app) or locally (cwd = repo root, script in space/).
|
| 38 |
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 39 |
+
from knowledge import ( # noqa: E402
|
| 40 |
+
KnowledgePipeline,
|
| 41 |
+
generate_training_notebook,
|
| 42 |
+
import_adapter as import_adapter_files,
|
| 43 |
+
)
|
| 44 |
|
| 45 |
|
| 46 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 587 |
return f"ποΈ Cleared {n} document(s).", render_knowledge_table()
|
| 588 |
|
| 589 |
|
| 590 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 591 |
+
# LoRA-on-upload (Phase 4b)
|
| 592 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 593 |
+
|
| 594 |
+
NOTEBOOK_DIR = Path("data/notebooks")
|
| 595 |
+
NOTEBOOK_DIR.mkdir(parents=True, exist_ok=True)
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def handle_generate_notebook(
|
| 599 |
+
selected_doc_names: list[str],
|
| 600 |
+
adapter_name: str,
|
| 601 |
+
lora_rank: int,
|
| 602 |
+
num_epochs: int,
|
| 603 |
+
):
|
| 604 |
+
"""Build a Colab notebook from the chunks of the selected documents."""
|
| 605 |
+
if not selected_doc_names:
|
| 606 |
+
return "_Select at least one indexed document first._", None
|
| 607 |
+
adapter_name = (adapter_name or "").strip() or "user_adapter"
|
| 608 |
+
|
| 609 |
+
# Gather chunks for the selected docs from the store.
|
| 610 |
+
with KNOWLEDGE.store._conn() as c: # noqa: SLF001 β internal access OK for now
|
| 611 |
+
rows = c.execute(
|
| 612 |
+
"SELECT documents.name as name, chunks.text as text "
|
| 613 |
+
"FROM chunks JOIN documents ON chunks.document_id = documents.id "
|
| 614 |
+
"WHERE documents.name IN (" + ",".join(["?"] * len(selected_doc_names)) + ") "
|
| 615 |
+
"ORDER BY chunks.document_id, chunks.chunk_index",
|
| 616 |
+
selected_doc_names,
|
| 617 |
+
).fetchall()
|
| 618 |
+
chunks = [r["text"] for r in rows]
|
| 619 |
+
if not chunks:
|
| 620 |
+
return "_No chunks found for those documents β re-index them?_", None
|
| 621 |
+
|
| 622 |
+
safe_name = "".join(ch if ch.isalnum() else "_" for ch in adapter_name)[:40] or "user_adapter"
|
| 623 |
+
out_path = NOTEBOOK_DIR / f"evollm_train_{safe_name}.ipynb"
|
| 624 |
+
|
| 625 |
+
generate_training_notebook(
|
| 626 |
+
adapter_name=adapter_name,
|
| 627 |
+
chunks=chunks,
|
| 628 |
+
source_doc_names=selected_doc_names,
|
| 629 |
+
lora_rank=int(lora_rank),
|
| 630 |
+
num_epochs=int(num_epochs),
|
| 631 |
+
output_path=out_path,
|
| 632 |
+
description=f"Trained from {len(selected_doc_names)} document(s) via EvoLLM",
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
log_evolution(
|
| 636 |
+
"knowledge",
|
| 637 |
+
f"π Generated training notebook for '{adapter_name}' "
|
| 638 |
+
f"({len(chunks)} chunks, {len(selected_doc_names)} doc(s))",
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
msg = (
|
| 642 |
+
f"β
**Notebook ready**: `{out_path.name}` ({len(chunks)} training examples).\n\n"
|
| 643 |
+
f"1. Download the file below\n"
|
| 644 |
+
f"2. Open it in [Google Colab](https://colab.research.google.com/)\n"
|
| 645 |
+
f"3. **Runtime β Change runtime type β T4 GPU**, then **Runtime β Run all**\n"
|
| 646 |
+
f"4. After training, download the two output files (`*.gguf` and `*.json`)\n"
|
| 647 |
+
f"5. Come back here, go to **𧬠Adapter Pool** tab β **π₯ Import trained adapter**"
|
| 648 |
+
)
|
| 649 |
+
return msg, str(out_path)
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def handle_import_adapter(gguf_file, manifest_file):
|
| 653 |
+
"""Receive a trained LoRA + manifest, register a new adapter in the pool."""
|
| 654 |
+
if not gguf_file or not manifest_file:
|
| 655 |
+
return "_Drop both the .gguf and the .json files._", render_pool_table()
|
| 656 |
+
try:
|
| 657 |
+
gguf_path = gguf_file if isinstance(gguf_file, str) else gguf_file.name
|
| 658 |
+
manifest_path = manifest_file if isinstance(manifest_file, str) else manifest_file.name
|
| 659 |
+
info = import_adapter_files(gguf_path, manifest_path)
|
| 660 |
+
except Exception as e:
|
| 661 |
+
return f"β Import failed: {e}", render_pool_table()
|
| 662 |
+
|
| 663 |
+
manifest = info["manifest"]
|
| 664 |
+
adapter_id = info["adapter_id"]
|
| 665 |
+
|
| 666 |
+
# Build a genome reflecting the trained-from-knowledge adapter.
|
| 667 |
+
sys_prompt = (
|
| 668 |
+
f"You are EvoLLM, fine-tuned on user-provided documents "
|
| 669 |
+
f"({', '.join(manifest.get('source_documents', []))[:160] or 'unknown sources'}). "
|
| 670 |
+
f"Draw on what you learned from those sources when relevant."
|
| 671 |
+
)
|
| 672 |
+
genome = Genome(
|
| 673 |
+
genome_id=adapter_id,
|
| 674 |
+
parent_id="evo_default",
|
| 675 |
+
generation=1,
|
| 676 |
+
name=info["name"],
|
| 677 |
+
base_model=manifest.get("base_model", "SmolLM2-1.7B-Instruct"),
|
| 678 |
+
lora_rank=int(manifest.get("lora_rank", 16)),
|
| 679 |
+
lora_alpha=int(manifest.get("lora_alpha", 32)),
|
| 680 |
+
lora_target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
|
| 681 |
+
system_prompt=sys_prompt,
|
| 682 |
+
eval_bank_score=None, # unmeasured until the eval bank runs against it
|
| 683 |
+
)
|
| 684 |
+
new_adapter = Adapter(
|
| 685 |
+
adapter_id=adapter_id,
|
| 686 |
+
name=info["name"],
|
| 687 |
+
description=info["description"] or "User-trained from documents",
|
| 688 |
+
genome=genome,
|
| 689 |
+
promoted=True,
|
| 690 |
+
)
|
| 691 |
+
POOL.append(new_adapter)
|
| 692 |
+
POOL_BY_ID[adapter_id] = new_adapter
|
| 693 |
+
BANDIT.register(adapter_id, prior_fitness=0.55) # neutral-mid prior
|
| 694 |
+
|
| 695 |
+
log_evolution(
|
| 696 |
+
"promotion",
|
| 697 |
+
f"π₯ IMPORTED user-trained adapter '{info['name']}' "
|
| 698 |
+
f"({manifest.get('training_examples', '?')} examples, rank {genome.lora_rank}) β joined the pool.",
|
| 699 |
+
{"adapter_id": adapter_id, "source_documents": manifest.get("source_documents", [])},
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
note = (
|
| 703 |
+
f"β
**{info['name']}** imported and added to the adapter pool.\n\n"
|
| 704 |
+
f"_GGUF saved to `{info['gguf_path']}`. The Bandit will start sampling it on the next "
|
| 705 |
+
f"chat. Real LoRA weight-loading is active in the local desktop app; on this Space the "
|
| 706 |
+
f"adapter uses its trained-from-data genome (system prompt + sampling config)._"
|
| 707 |
+
)
|
| 708 |
+
return note, render_pool_table()
|
| 709 |
+
|
| 710 |
+
|
| 711 |
def refresh_all():
|
| 712 |
return (
|
| 713 |
render_active_genome(),
|
|
|
|
| 827 |
refresh_btn = gr.Button("π Refresh")
|
| 828 |
evolution_result = gr.Markdown("")
|
| 829 |
|
| 830 |
+
gr.Markdown("---")
|
| 831 |
+
gr.Markdown(
|
| 832 |
+
"### π₯ Import a trained adapter\n"
|
| 833 |
+
"Drop the two files produced by the Colab training notebook "
|
| 834 |
+
"(`*.gguf` and `*.json`) to add a user-trained adapter to the pool."
|
| 835 |
+
)
|
| 836 |
+
with gr.Row():
|
| 837 |
+
import_gguf = gr.File(
|
| 838 |
+
label="LoRA adapter (.gguf)", file_types=[".gguf"], type="filepath",
|
| 839 |
+
)
|
| 840 |
+
import_manifest = gr.File(
|
| 841 |
+
label="Manifest (.json)", file_types=[".json"], type="filepath",
|
| 842 |
+
)
|
| 843 |
+
import_btn = gr.Button("π₯ Import adapter", variant="primary")
|
| 844 |
+
import_status = gr.Markdown("")
|
| 845 |
+
|
| 846 |
# ββ Tab 3: Knowledge βββββββββββββββββββββββββββββββββββββββββ
|
| 847 |
with gr.Tab("π Knowledge"):
|
| 848 |
gr.Markdown(
|
|
|
|
| 893 |
clear_knowledge_btn = gr.Button("ποΈ Clear all documents", variant="stop")
|
| 894 |
knowledge_action_status = gr.Markdown("")
|
| 895 |
|
| 896 |
+
gr.Markdown("---")
|
| 897 |
+
gr.Markdown(
|
| 898 |
+
"### 𧬠Train an adapter from these documents\n"
|
| 899 |
+
"Bake the document content into a real LoRA adapter via QLoRA on Colab. "
|
| 900 |
+
"EvoLLM generates a configured notebook with your data inline; you run it "
|
| 901 |
+
"on a free T4 GPU; then import the resulting `.gguf` + manifest back here."
|
| 902 |
+
)
|
| 903 |
+
with gr.Row():
|
| 904 |
+
with gr.Column(scale=2):
|
| 905 |
+
train_doc_select = gr.CheckboxGroup(
|
| 906 |
+
choices=[d["name"] for d in KNOWLEDGE.documents()],
|
| 907 |
+
label="Documents to train on",
|
| 908 |
+
info="Select one or more indexed documents.",
|
| 909 |
+
)
|
| 910 |
+
with gr.Column(scale=1):
|
| 911 |
+
train_adapter_name = gr.Textbox(
|
| 912 |
+
label="Adapter name", placeholder="e.g. company_handbook",
|
| 913 |
+
)
|
| 914 |
+
train_lora_rank = gr.Slider(
|
| 915 |
+
minimum=4, maximum=64, value=16, step=4,
|
| 916 |
+
label="LoRA rank",
|
| 917 |
+
info="Higher = more capacity, slower training",
|
| 918 |
+
)
|
| 919 |
+
train_num_epochs = gr.Slider(
|
| 920 |
+
minimum=1, maximum=10, value=3, step=1,
|
| 921 |
+
label="Training epochs",
|
| 922 |
+
)
|
| 923 |
+
with gr.Row():
|
| 924 |
+
refresh_train_docs_btn = gr.Button("π Refresh doc list", size="sm")
|
| 925 |
+
generate_notebook_btn = gr.Button("𧬠Generate training notebook", variant="primary")
|
| 926 |
+
notebook_status = gr.Markdown("")
|
| 927 |
+
notebook_download = gr.File(label="π Download notebook", interactive=False)
|
| 928 |
+
|
| 929 |
# ββ Tab 4: Evolution Log βββββββββββββββββββββββββββββββββββββ
|
| 930 |
with gr.Tab("π Evolution Log"):
|
| 931 |
gr.Markdown("### Lineage of mutations, promotions, and feedback events")
|
|
|
|
| 1070 |
api_name=False,
|
| 1071 |
).then(render_knowledge_stats, None, knowledge_stats, api_name=False)
|
| 1072 |
|
| 1073 |
+
# Refresh the doc selector when documents change (keep choices in sync)
|
| 1074 |
+
def _refresh_doc_choices():
|
| 1075 |
+
return gr.update(choices=[d["name"] for d in KNOWLEDGE.documents()])
|
| 1076 |
+
|
| 1077 |
+
refresh_train_docs_btn.click(
|
| 1078 |
+
_refresh_doc_choices, None, train_doc_select, api_name=False,
|
| 1079 |
+
)
|
| 1080 |
+
|
| 1081 |
+
generate_notebook_btn.click(
|
| 1082 |
+
handle_generate_notebook,
|
| 1083 |
+
[train_doc_select, train_adapter_name, train_lora_rank, train_num_epochs],
|
| 1084 |
+
[notebook_status, notebook_download],
|
| 1085 |
+
api_name=False,
|
| 1086 |
+
)
|
| 1087 |
+
|
| 1088 |
+
import_btn.click(
|
| 1089 |
+
handle_import_adapter,
|
| 1090 |
+
[import_gguf, import_manifest],
|
| 1091 |
+
[import_status, pool_table],
|
| 1092 |
+
api_name=False,
|
| 1093 |
+
).then(refresh_all, None, refresh_outputs, api_name=False)
|
| 1094 |
+
|
| 1095 |
|
| 1096 |
if __name__ == "__main__":
|
| 1097 |
demo.queue().launch(
|
knowledge/__init__.py
CHANGED
|
@@ -1,18 +1,31 @@
|
|
| 1 |
-
"""EvoLLM knowledge layer β RAG
|
| 2 |
|
| 3 |
Components:
|
| 4 |
-
- parser
|
| 5 |
-
- chunker
|
| 6 |
-
- embedder
|
| 7 |
-
- store
|
| 8 |
-
- pipeline
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
Local persistence at data/knowledge.sqlite. On HF Spaces
|
| 11 |
-
container's ephemeral disk, so uploads vanish on rebuild
|
| 12 |
-
notice in the UI explaining that.
|
| 13 |
"""
|
| 14 |
|
|
|
|
|
|
|
|
|
|
| 15 |
from .pipeline import KnowledgePipeline
|
| 16 |
from .store import KnowledgeStore
|
| 17 |
|
| 18 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""EvoLLM knowledge layer β RAG + LoRA-on-upload.
|
| 2 |
|
| 3 |
Components:
|
| 4 |
+
- parser : PDF / TXT / MD / DOCX β clean text
|
| 5 |
+
- chunker : long text β overlapping ~400-token chunks
|
| 6 |
+
- embedder : fastembed wrapper (no torch dependency)
|
| 7 |
+
- store : SQLite + numpy vector store
|
| 8 |
+
- pipeline : high-level ingest / query API
|
| 9 |
+
- dataset_builder : doc chunks β training JSONL
|
| 10 |
+
- notebook_generator : produce a Colab notebook with the dataset baked in
|
| 11 |
+
- adapter_importer : accept uploaded GGUF + manifest, register an adapter
|
| 12 |
|
| 13 |
+
Local persistence at data/knowledge.sqlite + data/adapters/. On HF Spaces
|
| 14 |
+
the path is the container's ephemeral disk, so uploads vanish on rebuild
|
| 15 |
+
β there's a visible notice in the UI explaining that.
|
| 16 |
"""
|
| 17 |
|
| 18 |
+
from .adapter_importer import import_adapter
|
| 19 |
+
from .dataset_builder import build_dataset, write_jsonl
|
| 20 |
+
from .notebook_generator import generate_training_notebook
|
| 21 |
from .pipeline import KnowledgePipeline
|
| 22 |
from .store import KnowledgeStore
|
| 23 |
|
| 24 |
+
__all__ = [
|
| 25 |
+
"KnowledgePipeline",
|
| 26 |
+
"KnowledgeStore",
|
| 27 |
+
"build_dataset",
|
| 28 |
+
"write_jsonl",
|
| 29 |
+
"generate_training_notebook",
|
| 30 |
+
"import_adapter",
|
| 31 |
+
]
|
knowledge/adapter_importer.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Import a trained LoRA adapter (GGUF + manifest) back into EvoLLM.
|
| 2 |
+
|
| 3 |
+
The Colab notebook produced by notebook_generator.py emits two files
|
| 4 |
+
at the end of a training run. The user re-uploads them via the
|
| 5 |
+
'Import trained adapter' UI; we validate them, stash the GGUF, parse
|
| 6 |
+
the manifest, and surface an AdapterRecord-shaped dict so the caller
|
| 7 |
+
can register it in the live POOL + BANDIT.
|
| 8 |
+
|
| 9 |
+
We don't actually load the LoRA into llama.cpp here β that happens in
|
| 10 |
+
the local desktop app where reloading the Llama instance with
|
| 11 |
+
lora_path=... is acceptable. On the HF Space the imported adapter
|
| 12 |
+
joins the pool with its genome (system prompt + sampling), and the
|
| 13 |
+
bandit can pick it like any other variant; the GGUF and manifest are
|
| 14 |
+
stored for the local app's use.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import json
|
| 20 |
+
import shutil
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
DEFAULT_ADAPTER_DIR = Path("data/adapters")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def import_adapter(
|
| 27 |
+
gguf_path: str | Path,
|
| 28 |
+
manifest_path: str | Path,
|
| 29 |
+
dest_dir: str | Path = DEFAULT_ADAPTER_DIR,
|
| 30 |
+
) -> dict:
|
| 31 |
+
"""Validate + copy + return adapter info."""
|
| 32 |
+
gguf_path = Path(gguf_path)
|
| 33 |
+
manifest_path = Path(manifest_path)
|
| 34 |
+
dest_dir = Path(dest_dir)
|
| 35 |
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
| 36 |
+
|
| 37 |
+
if not gguf_path.exists():
|
| 38 |
+
raise FileNotFoundError(f"Missing GGUF: {gguf_path}")
|
| 39 |
+
if not manifest_path.exists():
|
| 40 |
+
raise FileNotFoundError(f"Missing manifest: {manifest_path}")
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 44 |
+
except json.JSONDecodeError as e:
|
| 45 |
+
raise ValueError(f"Manifest is not valid JSON: {e}") from e
|
| 46 |
+
|
| 47 |
+
for required in ("adapter_id", "name", "base_model"):
|
| 48 |
+
if required not in manifest:
|
| 49 |
+
raise ValueError(f"Manifest missing required field: {required}")
|
| 50 |
+
|
| 51 |
+
adapter_id = manifest["adapter_id"]
|
| 52 |
+
dest_gguf = dest_dir / f"{adapter_id}.gguf"
|
| 53 |
+
dest_manifest = dest_dir / f"{adapter_id}.json"
|
| 54 |
+
|
| 55 |
+
shutil.copy(gguf_path, dest_gguf)
|
| 56 |
+
shutil.copy(manifest_path, dest_manifest)
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
"adapter_id": adapter_id,
|
| 60 |
+
"name": manifest["name"],
|
| 61 |
+
"description": manifest.get("description", ""),
|
| 62 |
+
"base_model": manifest["base_model"],
|
| 63 |
+
"gguf_path": str(dest_gguf),
|
| 64 |
+
"manifest_path": str(dest_manifest),
|
| 65 |
+
"manifest": manifest,
|
| 66 |
+
}
|
knowledge/dataset_builder.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert indexed document chunks into a training dataset.
|
| 2 |
+
|
| 3 |
+
Two output formats:
|
| 4 |
+
β’ text mode : plain-LM continued pretraining ({"text": "..."} per row).
|
| 5 |
+
Best for vocabulary/language acquisition (e.g. Creole).
|
| 6 |
+
No teacher model required.
|
| 7 |
+
β’ instruction : chat-style (system/user/assistant) where the user asks
|
| 8 |
+
for content and the assistant emits the chunk text.
|
| 9 |
+
Better for domain Q&A; still no teacher needed β
|
| 10 |
+
the chunk content itself is the target.
|
| 11 |
+
|
| 12 |
+
The pipeline writes JSONL because that's what trl.SFTTrainer ingests
|
| 13 |
+
directly.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Iterable, Literal
|
| 21 |
+
|
| 22 |
+
DatasetMode = Literal["text", "instruction"]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def build_dataset(
|
| 26 |
+
chunks: Iterable[str],
|
| 27 |
+
mode: DatasetMode = "text",
|
| 28 |
+
system_prompt: str = "You are EvoLLM, fine-tuned on user-provided documents.",
|
| 29 |
+
) -> list[dict]:
|
| 30 |
+
chunks = [c.strip() for c in chunks if c and c.strip()]
|
| 31 |
+
if mode == "text":
|
| 32 |
+
return [{"text": c} for c in chunks]
|
| 33 |
+
if mode == "instruction":
|
| 34 |
+
return [
|
| 35 |
+
{
|
| 36 |
+
"messages": [
|
| 37 |
+
{"role": "system", "content": system_prompt},
|
| 38 |
+
{"role": "user", "content": "Continue the passage in the style of the source material."},
|
| 39 |
+
{"role": "assistant", "content": c},
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
for c in chunks
|
| 43 |
+
]
|
| 44 |
+
raise ValueError(f"Unknown dataset mode: {mode}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def write_jsonl(rows: list[dict], path: str | Path) -> Path:
|
| 48 |
+
path = Path(path)
|
| 49 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
with path.open("w", encoding="utf-8") as f:
|
| 51 |
+
for row in rows:
|
| 52 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 53 |
+
return path
|
knowledge/notebook_generator.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate a self-contained Colab notebook that QLoRA-trains an adapter
|
| 2 |
+
on the user's selected document chunks.
|
| 3 |
+
|
| 4 |
+
The user downloads the .ipynb from the EvoLLM UI, opens it in Colab
|
| 5 |
+
(free T4 is sufficient for small corpora), clicks 'Run all', and
|
| 6 |
+
downloads two files at the end: the LoRA adapter as GGUF and a
|
| 7 |
+
manifest.json. They re-upload both into EvoLLM via the 'Import trained
|
| 8 |
+
adapter' button, and the new adapter joins the pool.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import uuid
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _cell(cell_type: str, source: str) -> dict:
|
| 20 |
+
return {
|
| 21 |
+
"cell_type": cell_type,
|
| 22 |
+
"metadata": {},
|
| 23 |
+
"source": source.splitlines(keepends=True),
|
| 24 |
+
**({"execution_count": None, "outputs": []} if cell_type == "code" else {}),
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def generate_training_notebook(
|
| 29 |
+
adapter_name: str,
|
| 30 |
+
chunks: list[str],
|
| 31 |
+
source_doc_names: list[str],
|
| 32 |
+
base_model: str = "HuggingFaceTB/SmolLM2-1.7B-Instruct",
|
| 33 |
+
lora_rank: int = 16,
|
| 34 |
+
lora_alpha: int | None = None,
|
| 35 |
+
learning_rate: float = 2e-4,
|
| 36 |
+
num_epochs: int = 3,
|
| 37 |
+
batch_size: int = 2,
|
| 38 |
+
grad_accum: int = 4,
|
| 39 |
+
output_path: str | Path = "evollm_training_notebook.ipynb",
|
| 40 |
+
description: str = "",
|
| 41 |
+
) -> Path:
|
| 42 |
+
"""Produce a configured .ipynb the user can run on Colab."""
|
| 43 |
+
if lora_alpha is None:
|
| 44 |
+
lora_alpha = lora_rank * 2
|
| 45 |
+
|
| 46 |
+
adapter_id = f"user_{uuid.uuid4().hex[:8]}"
|
| 47 |
+
safe_adapter_name = adapter_name.strip() or adapter_id
|
| 48 |
+
created_at = datetime.utcnow().isoformat()
|
| 49 |
+
|
| 50 |
+
dataset_rows = [{"text": c} for c in chunks if c and c.strip()]
|
| 51 |
+
|
| 52 |
+
manifest = {
|
| 53 |
+
"adapter_id": adapter_id,
|
| 54 |
+
"name": safe_adapter_name,
|
| 55 |
+
"description": description or f"User-trained adapter on {len(source_doc_names)} document(s)",
|
| 56 |
+
"base_model": base_model,
|
| 57 |
+
"source_documents": source_doc_names,
|
| 58 |
+
"lora_rank": lora_rank,
|
| 59 |
+
"lora_alpha": lora_alpha,
|
| 60 |
+
"learning_rate": learning_rate,
|
| 61 |
+
"num_epochs": num_epochs,
|
| 62 |
+
"training_examples": len(dataset_rows),
|
| 63 |
+
"trained_at": created_at,
|
| 64 |
+
"trained_from_knowledge": True,
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
intro_md = f"""# EvoLLM β Train your own adapter
|
| 68 |
+
|
| 69 |
+
This notebook produces a **{safe_adapter_name}** LoRA adapter from your
|
| 70 |
+
selected documents.
|
| 71 |
+
|
| 72 |
+
**Source documents**: {", ".join(source_doc_names) or "(none)"}
|
| 73 |
+
**Base model**: `{base_model}`
|
| 74 |
+
**LoRA rank**: {lora_rank} (alpha = {lora_alpha})
|
| 75 |
+
**Epochs**: {num_epochs} Β· **LR**: {learning_rate} Β· **Examples**: {len(dataset_rows)}
|
| 76 |
+
|
| 77 |
+
## How to run
|
| 78 |
+
|
| 79 |
+
1. **Runtime β Change runtime type β T4 GPU** (or A100 if you have Colab Pro).
|
| 80 |
+
2. Click **Runtime β Run all**.
|
| 81 |
+
3. When training finishes, you'll get two download links:
|
| 82 |
+
- `{adapter_id}.gguf` β the LoRA adapter in llama.cpp format
|
| 83 |
+
- `{adapter_id}.json` β the manifest
|
| 84 |
+
4. Back in EvoLLM, go to the **𧬠Adapter Pool** tab β **π₯ Import trained adapter** β drop both files.
|
| 85 |
+
|
| 86 |
+
Approximate runtime on free T4: ~20β60 minutes for {len(dataset_rows)} examples.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
setup_code = """!nvidia-smi
|
| 90 |
+
!pip install -q -U \\
|
| 91 |
+
"transformers>=4.46" "peft>=0.13" "trl>=0.12" \\
|
| 92 |
+
"datasets>=3.1" "accelerate>=1.1" "bitsandbytes>=0.44" "sentencepiece"
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
config_code = f"""import json, gc, torch
|
| 96 |
+
from pathlib import Path
|
| 97 |
+
|
| 98 |
+
ADAPTER_ID = "{adapter_id}"
|
| 99 |
+
ADAPTER_NAME = {json.dumps(safe_adapter_name)}
|
| 100 |
+
BASE_MODEL = {json.dumps(base_model)}
|
| 101 |
+
LORA_RANK = {lora_rank}
|
| 102 |
+
LORA_ALPHA = {lora_alpha}
|
| 103 |
+
LEARNING_RATE = {learning_rate}
|
| 104 |
+
NUM_EPOCHS = {num_epochs}
|
| 105 |
+
BATCH_SIZE = {batch_size}
|
| 106 |
+
GRAD_ACCUM = {grad_accum}
|
| 107 |
+
|
| 108 |
+
OUT_DIR = Path(f"/content/{{ADAPTER_ID}}")
|
| 109 |
+
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
# Inline the dataset as a JSON list. For small corpora this is fine;
|
| 113 |
+
# very large corpora should switch to a side file, but that's edge case.
|
| 114 |
+
dataset_code = "DATASET_ROWS = " + json.dumps(dataset_rows, ensure_ascii=False, indent=2)
|
| 115 |
+
|
| 116 |
+
manifest_code = (
|
| 117 |
+
"MANIFEST = " + json.dumps(manifest, ensure_ascii=False, indent=2)
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
train_code = """from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 121 |
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
| 122 |
+
from trl import SFTTrainer, SFTConfig
|
| 123 |
+
from datasets import Dataset
|
| 124 |
+
|
| 125 |
+
bnb = BitsAndBytesConfig(
|
| 126 |
+
load_in_4bit=True,
|
| 127 |
+
bnb_4bit_quant_type="nf4",
|
| 128 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 129 |
+
bnb_4bit_use_double_quant=True,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
print(f"Loading base: {BASE_MODEL}")
|
| 133 |
+
tok = AutoTokenizer.from_pretrained(BASE_MODEL)
|
| 134 |
+
if tok.pad_token is None:
|
| 135 |
+
tok.pad_token = tok.eos_token
|
| 136 |
+
|
| 137 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 138 |
+
BASE_MODEL, quantization_config=bnb, device_map="auto", torch_dtype=torch.bfloat16,
|
| 139 |
+
)
|
| 140 |
+
model = prepare_model_for_kbit_training(model)
|
| 141 |
+
|
| 142 |
+
peft_cfg = LoraConfig(
|
| 143 |
+
r=LORA_RANK, lora_alpha=LORA_ALPHA, lora_dropout=0.05,
|
| 144 |
+
bias="none", task_type="CAUSAL_LM",
|
| 145 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
|
| 146 |
+
)
|
| 147 |
+
model = get_peft_model(model, peft_cfg)
|
| 148 |
+
model.print_trainable_parameters()
|
| 149 |
+
|
| 150 |
+
ds = Dataset.from_list(DATASET_ROWS)
|
| 151 |
+
|
| 152 |
+
cfg = SFTConfig(
|
| 153 |
+
output_dir=str(OUT_DIR),
|
| 154 |
+
num_train_epochs=NUM_EPOCHS,
|
| 155 |
+
per_device_train_batch_size=BATCH_SIZE,
|
| 156 |
+
gradient_accumulation_steps=GRAD_ACCUM,
|
| 157 |
+
learning_rate=LEARNING_RATE,
|
| 158 |
+
bf16=True,
|
| 159 |
+
logging_steps=10,
|
| 160 |
+
save_strategy="epoch",
|
| 161 |
+
save_total_limit=1,
|
| 162 |
+
report_to="none",
|
| 163 |
+
max_seq_length=1024,
|
| 164 |
+
warmup_ratio=0.03,
|
| 165 |
+
dataset_text_field="text",
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
trainer = SFTTrainer(model=model, tokenizer=tok, train_dataset=ds, args=cfg)
|
| 169 |
+
trainer.train()
|
| 170 |
+
trainer.save_model(str(OUT_DIR))
|
| 171 |
+
print(f"\\nAdapter saved to {OUT_DIR}")
|
| 172 |
+
|
| 173 |
+
del model, trainer; gc.collect(); torch.cuda.empty_cache()
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
convert_code = """# Convert the LoRA adapter to GGUF for llama.cpp
|
| 177 |
+
!apt-get install -y -qq cmake build-essential
|
| 178 |
+
!git clone --depth 1 https://github.com/ggerganov/llama.cpp /content/llama.cpp 2>/dev/null || echo 'already cloned'
|
| 179 |
+
!pip install -q -r /content/llama.cpp/requirements/requirements-convert_lora_to_gguf.txt
|
| 180 |
+
|
| 181 |
+
GGUF_PATH = OUT_DIR / f"{ADAPTER_ID}.gguf"
|
| 182 |
+
!python /content/llama.cpp/convert_lora_to_gguf.py {OUT_DIR} --base {BASE_MODEL} --outfile {GGUF_PATH}
|
| 183 |
+
|
| 184 |
+
print(f"\\nGGUF adapter at: {GGUF_PATH}")
|
| 185 |
+
print(f"Size: {GGUF_PATH.stat().st_size / 1024 / 1024:.1f} MB")
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
package_code = """# Save manifest and prepare downloads
|
| 189 |
+
import shutil
|
| 190 |
+
|
| 191 |
+
manifest_path = OUT_DIR / f"{ADAPTER_ID}.json"
|
| 192 |
+
manifest_path.write_text(json.dumps(MANIFEST, ensure_ascii=False, indent=2))
|
| 193 |
+
|
| 194 |
+
# Stage the two files at /content for easy download
|
| 195 |
+
shutil.copy(GGUF_PATH, f"/content/{ADAPTER_ID}.gguf")
|
| 196 |
+
shutil.copy(manifest_path, f"/content/{ADAPTER_ID}.json")
|
| 197 |
+
|
| 198 |
+
print("\\n" + "=" * 60)
|
| 199 |
+
print("READY TO DOWNLOAD")
|
| 200 |
+
print("=" * 60)
|
| 201 |
+
print(f" /content/{ADAPTER_ID}.gguf")
|
| 202 |
+
print(f" /content/{ADAPTER_ID}.json")
|
| 203 |
+
print()
|
| 204 |
+
print("In the Colab Files panel (left side), right-click each file β Download.")
|
| 205 |
+
print("Then in EvoLLM: 𧬠Adapter Pool tab β π₯ Import trained adapter β drop both files.")
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
notebook = {
|
| 209 |
+
"cells": [
|
| 210 |
+
_cell("markdown", intro_md),
|
| 211 |
+
_cell("markdown", "## 0. Setup"),
|
| 212 |
+
_cell("code", setup_code),
|
| 213 |
+
_cell("markdown", "## 1. Configuration & dataset"),
|
| 214 |
+
_cell("code", config_code),
|
| 215 |
+
_cell("code", dataset_code),
|
| 216 |
+
_cell("code", manifest_code),
|
| 217 |
+
_cell("markdown", "## 2. Train"),
|
| 218 |
+
_cell("code", train_code),
|
| 219 |
+
_cell("markdown", "## 3. Convert to GGUF"),
|
| 220 |
+
_cell("code", convert_code),
|
| 221 |
+
_cell("markdown", "## 4. Package for EvoLLM"),
|
| 222 |
+
_cell("code", package_code),
|
| 223 |
+
],
|
| 224 |
+
"metadata": {
|
| 225 |
+
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
|
| 226 |
+
"language_info": {"name": "python", "version": "3.10"},
|
| 227 |
+
"accelerator": "GPU",
|
| 228 |
+
"colab": {"provenance": [], "gpuType": "T4"},
|
| 229 |
+
},
|
| 230 |
+
"nbformat": 4,
|
| 231 |
+
"nbformat_minor": 4,
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
output_path = Path(output_path)
|
| 235 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 236 |
+
output_path.write_text(json.dumps(notebook, ensure_ascii=False, indent=1))
|
| 237 |
+
return output_path
|