Text Generation
Transformers
Safetensors
GGUF
English
gemma3_text
tiny-model
gemma3
from-scratch
conversational
tool-use
agent-harness
calculator
humble-ai
small-language-model
muon
on-device
experimental
text-generation-inference
Instructions to use textilelabs/Loom-Swift-Micro-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use textilelabs/Loom-Swift-Micro-2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="textilelabs/Loom-Swift-Micro-2")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("textilelabs/Loom-Swift-Micro-2") model = AutoModelForCausalLM.from_pretrained("textilelabs/Loom-Swift-Micro-2", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use textilelabs/Loom-Swift-Micro-2 with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M # Run inference directly in the terminal: llama cli -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M # Run inference directly in the terminal: llama cli -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Use Docker
docker model run hf.co/textilelabs/Loom-Swift-Micro-2:Q4_K_M
- Llama new
- LM Studio
- Jan
- vLLM
How to use textilelabs/Loom-Swift-Micro-2 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "textilelabs/Loom-Swift-Micro-2" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Swift-Micro-2", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/textilelabs/Loom-Swift-Micro-2:Q4_K_M
- SGLang
How to use textilelabs/Loom-Swift-Micro-2 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "textilelabs/Loom-Swift-Micro-2" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Swift-Micro-2", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "textilelabs/Loom-Swift-Micro-2" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Swift-Micro-2", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Ollama
How to use textilelabs/Loom-Swift-Micro-2 with Ollama:
ollama run hf.co/textilelabs/Loom-Swift-Micro-2:Q4_K_M
- Unsloth Desktop
- Pi
How to use textilelabs/Loom-Swift-Micro-2 with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "textilelabs/Loom-Swift-Micro-2:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use textilelabs/Loom-Swift-Micro-2 with Docker Model Runner:
docker model run hf.co/textilelabs/Loom-Swift-Micro-2:Q4_K_M
- Lemonade
How to use textilelabs/Loom-Swift-Micro-2 with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull textilelabs/Loom-Swift-Micro-2:Q4_K_M
Run and chat with the model
lemonade run user.Loom-Swift-Micro-2-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use textilelabs/Loom-Swift-Micro-2 with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default textilelabs/Loom-Swift-Micro-2:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use textilelabs/Loom-Swift-Micro-2 with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf textilelabs/Loom-Swift-Micro-2:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "textilelabs/Loom-Swift-Micro-2:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Upload 18 files
Browse files- .gitattributes +5 -0
- ATTRIBUTION.md +20 -0
- LICENSE +21 -0
- Modelfile +16 -0
- README.md +193 -0
- banner.jpeg +3 -0
- config.json +56 -0
- generation_config.json +9 -0
- harness.py +275 -0
- logo.jpeg +3 -0
- loom-swift-micro-2-f16.gguf +3 -0
- loom-swift-micro-2-q4_k_m.gguf +3 -0
- loom-swift-micro-2-q8_0.gguf +3 -0
- model.safetensors +3 -0
- params +8 -0
- special_tokens_map.json +22 -0
- template +5 -0
- tokenizer.json +0 -0
- tokenizer_config.json +11 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
banner.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
logo.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
loom-swift-micro-2-f16.gguf filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
loom-swift-micro-2-q4_k_m.gguf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
loom-swift-micro-2-q8_0.gguf filter=lfs diff=lfs merge=lfs -text
|
ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Attribution
|
| 2 |
+
|
| 3 |
+
Loom Swift 2 (Mini and Micro) were trained from scratch: randomly initialised weights, no third-party checkpoint. Each ran for a
|
| 4 |
+
fixed few minutes on two NVIDIA T4s. Some licences below require attribution; this file satisfies that and must be kept with any
|
| 5 |
+
redistribution.
|
| 6 |
+
|
| 7 |
+
| slice | source | licence |
|
| 8 |
+
|---|---|---|
|
| 9 |
+
| topic labelling | DBpedia-14 (fancyzhx/dbpedia_14) | CC BY-SA 3.0 |
|
| 10 |
+
| sentiment labelling | Amazon Polarity (fancyzhx/amazon_polarity) | Apache 2.0 |
|
| 11 |
+
| toxicity labelling | Civil Comments (google/civil_comments) | CC0 1.0 |
|
| 12 |
+
| language labelling, request wording | MASSIVE (AmazonScience/massive) | CC BY 4.0 |
|
| 13 |
+
| 757 kinds of instruction | Super-NaturalInstructions | Apache 2.0 |
|
| 14 |
+
| human explanations of entailment | e-SNLI | MIT |
|
| 15 |
+
| common-sense questions | CommonsenseQA (MIT), ARC (CC BY-SA 4.0) | as listed |
|
| 16 |
+
| human question decompositions | StrategyQA | MIT |
|
| 17 |
+
| conversation replay (identity, limits, declines, tool use) | the Loom Weave 4 training mix: Wikipedia (CC BY-SA 3.0 / GFDL), SQuAD 2.0 (CC BY-SA 4.0), databricks-dolly-15k (CC BY-SA 3.0), OpenAssistant OASST1 (Apache 2.0), GSM8K (MIT) and Textile Labs program-generated data (MIT) | as listed |
|
| 18 |
+
| reading a search result, injection inside a passage, extraction, redaction, calculator and date calls, tool errors, everyday chat | Textile Labs (program-generated from Wikipedia text and program-checked) | MIT / CC BY-SA 3.0 for the Wikipedia text |
|
| 19 |
+
|
| 20 |
+
Model weights: MIT.
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Textile Labs
|
| 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.
|
Modelfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM ./loom-swift-micro-2-q8_0.gguf
|
| 2 |
+
TEMPLATE """<tools:off>
|
| 3 |
+
<user>
|
| 4 |
+
{{ .Prompt }}
|
| 5 |
+
<|eot|>
|
| 6 |
+
<loom>
|
| 7 |
+
"""
|
| 8 |
+
PARAMETER stop "<|eot|>"
|
| 9 |
+
PARAMETER stop "<user>"
|
| 10 |
+
PARAMETER stop "<result>"
|
| 11 |
+
PARAMETER stop "<output>"
|
| 12 |
+
PARAMETER temperature 0.7
|
| 13 |
+
PARAMETER top_k 40
|
| 14 |
+
PARAMETER repeat_penalty 1.0
|
| 15 |
+
PARAMETER num_predict 200
|
| 16 |
+
PARAMETER num_ctx 2048
|
README.md
CHANGED
|
@@ -1,3 +1,196 @@
|
|
| 1 |
---
|
| 2 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: mit
|
| 3 |
+
language: en
|
| 4 |
+
library_name: transformers
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
tags:
|
| 7 |
+
- tiny-model
|
| 8 |
+
- gemma3
|
| 9 |
+
- from-scratch
|
| 10 |
+
- conversational
|
| 11 |
+
- tool-use
|
| 12 |
+
- agent-harness
|
| 13 |
+
- calculator
|
| 14 |
+
- humble-ai
|
| 15 |
+
- small-language-model
|
| 16 |
+
- muon
|
| 17 |
+
- gguf
|
| 18 |
+
- on-device
|
| 19 |
+
- experimental
|
| 20 |
+
widget:
|
| 21 |
+
- text: "<tools:off>\n<user>\nwho are you\n<|eot|>\n<loom>\n"
|
| 22 |
+
example_title: "Identity"
|
| 23 |
+
- text: "<tools:on>\n<user>\nwhat's 482 + 539?\n<|eot|>\n<loom>\n"
|
| 24 |
+
example_title: "Writes a calculator call"
|
| 25 |
+
- text: "<tools:off>\n<user>\nwhat's the weather in Paris right now?\n<|eot|>\n<loom>\n"
|
| 26 |
+
example_title: "Limits"
|
| 27 |
---
|
| 28 |
+
|
| 29 |
+
<div align="center">
|
| 30 |
+
<img src="banner.jpg" alt="Loom Swift Micro 2" width="520">
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
# Loom Swift Micro 2
|
| 34 |
+
|
| 35 |
+
<img src="logo.jpg" alt="" width="20" height="20" style="border-radius:4px;vertical-align:middle;margin-right:6px;"> **1,172,832 parameters · 12 layers × 96d · 2K context · runs on phones · Textile Labs**
|
| 36 |
+
|
| 37 |
+
Loom Swift Micro 2 is a 1.17-million-parameter model trained for five minutes. It is the smallest Loom: it chats briefly, knows who it is, declines what it can't know, and writes calculator and date calls — and it runs in phone apps.
|
| 38 |
+
It is the second generation of [Loom Swift Micro](https://huggingface.co/textilelabs/Loom-Swift-Micro), retrained on Kaggle's two T4 GPUs for the same 5 minutes the original spent on a CPU,
|
| 39 |
+
and rebuilt on the Gemma-3 layout so it runs in PocketPal, LM Studio, Jan, llama.cpp and Ollama straight from the GGUF.
|
| 40 |
+
Trained from scratch: randomly initialised weights, nothing fine-tuned from anyone's checkpoint.
|
| 41 |
+
|
| 42 |
+
> ## Read this first
|
| 43 |
+
> This is a **tiny model for tiny jobs**, and it is honest about the limits we measured. It is not a reader and it is not a reasoner.
|
| 44 |
+
> What it does is: say who it is, decide when a tool is needed, write a calculator / date / search call, and (Mini) label and extract from short text.
|
| 45 |
+
> It is published as a small, fast, on-device experiment — not as a strict upgrade. The "Read this before you use it" section below lists every miss.
|
| 46 |
+
|
| 47 |
+
## What changed from the original
|
| 48 |
+
|
| 49 |
+
| | Loom Swift Micro | **Loom Swift Micro 2** |
|
| 50 |
+
|---|---|---|
|
| 51 |
+
| layout | Llama | **Gemma-3 style** (local sliding window + a global layer every 6th) |
|
| 52 |
+
| tokenizer | 4,096 BPE | **4,096 BPE rebuilt, every digit its own token, identical in llama.cpp** |
|
| 53 |
+
| context | 512 | **2,048** |
|
| 54 |
+
| runs in phone/desktop chat apps | no | **yes — chat template and end-of-turn built into the GGUF** |
|
| 55 |
+
| new jobs | — | calculator calls, date arithmetic, labelling, extraction, redaction, reading a search result |
|
| 56 |
+
| trained on | a 2013 office CPU | Kaggle 2 × T4 |
|
| 57 |
+
| training time | 10 / 5 minutes | the same: **5 minutes** of training |
|
| 58 |
+
|
| 59 |
+
## Measured against the original
|
| 60 |
+
|
| 61 |
+
Same tests, same scripts, same settings (repeat penalty 1.0), both through Ollama. Every test prompt is scrubbed from the training data.
|
| 62 |
+
|
| 63 |
+
**The Loom acceptance battery**, row by row:
|
| 64 |
+
|
| 65 |
+
| row | original | **Loom Swift Micro 2** |
|
| 66 |
+
|---|---:|---:|
|
| 67 |
+
| A · says its own name | 11/12 | 11/12 |
|
| 68 |
+
| B · its own name under rough typing | 11/12 | 11/12 |
|
| 69 |
+
| C · 5-turn conversation stays on thread | 5/5 | 5/5 |
|
| 70 |
+
| D · answers from a search result | 0/5 | 3/5 |
|
| 71 |
+
| E · follow-up answered from the same result | 0/5 | 0/5 |
|
| 72 |
+
| F · says it looked, after a lookup | 5/5 | 0/5 |
|
| 73 |
+
| G · **never** claims a lookup it didn't make | 16/16 | 16/16 |
|
| 74 |
+
| H · admits what it can't know about you | 7/8 | 5/8 |
|
| 75 |
+
| I · says when a result doesn't contain the answer | 0/5 | 3/5 |
|
| 76 |
+
| J · never leaks a search tag with tools off | 28/28 | 28/28 |
|
| 77 |
+
| K · stops on its own | 12/12 | 12/12 |
|
| 78 |
+
| L · searches when it should | 15/20 | 16/20 |
|
| 79 |
+
| **total** | **110/133** | **110/133** |
|
| 80 |
+
|
| 81 |
+
**New jobs** (our own frozen test set; the original Swifts never trained on most of these, so most of their zeros are expected):
|
| 82 |
+
|
| 83 |
+
| job (275 held-out items, each prompt scrubbed from training) | original Swift Micro | **Swift Micro 2** |
|
| 84 |
+
|---|---:|---:|
|
| 85 |
+
| everyday chat | 4/15 | **11/15** |
|
| 86 |
+
| declining an unknown fact, tools off (strict scorer) | 18/20* | 18/20 |
|
| 87 |
+
| ignoring an instruction inside a passage | 0/10 | **4/10** |
|
| 88 |
+
| labelling (topic / language / sentiment / toxic / route) | 0/60 | **28/60** |
|
| 89 |
+
| calculator call correct | 0/40 | **13/40** |
|
| 90 |
+
| date arithmetic | 0/10 | **3/10** |
|
| 91 |
+
| reading a retrieved passage | 0/60 | **4/60** |
|
| 92 |
+
| typed extraction | 0/30 | 0/30 |
|
| 93 |
+
| redaction (exact match) | 0/30 | 0/30 |
|
| 94 |
+
| saying "(sure)" and being right | — | 22% of 82 |
|
| 95 |
+
|
| 96 |
+
*the original was scored with a looser scorer.
|
| 97 |
+
|
| 98 |
+
Q8_0 and Q4_K_M behave the same within a few items (Q8: label 30/60, inject 5/10; Q4: label 27/60, inject 1/10 — injection resistance is the least stable part).
|
| 99 |
+
|
| 100 |
+
## Every Loom text model
|
| 101 |
+
|
| 102 |
+
| model | params | battery /133 | live search (held-out) | reads real prose | runs in phone apps |
|
| 103 |
+
|---|---:|---:|---:|---|---|
|
| 104 |
+
| Loom Spark 2 | 19.9M | ~97 | 2/20 | no | no |
|
| 105 |
+
| Loom Tapestry 2 | 22.8M | 107 | — | curated only | no |
|
| 106 |
+
| Loom Tapestry 3 Flash | 7.18M | 112 | 3/20 | curated only | no |
|
| 107 |
+
| Loom Spark 3 Flash | 7.18M | 119 | 5/20 | curated only | no |
|
| 108 |
+
| Loom Spark 3 | 12.2M | 120 | 7/20 | curated only | no |
|
| 109 |
+
| Loom Spark 3.2 | 22.8M | 122† | 7/20 | curated only | no |
|
| 110 |
+
| Loom Weave 3 | 31.5M | 120 | 6/20 | yes | no |
|
| 111 |
+
| Loom Tapestry 3 | 69.2M | 123 | 12/20 | yes + multi-hop | no |
|
| 112 |
+
| Loom Crucible Preview | 155.0M | 125† | 10/20 | yes — best curated reader | no |
|
| 113 |
+
| Loom Weave 4 | 55.6M | 127† | 10/20 | yes | yes |
|
| 114 |
+
| Loom Swift Mini (original) | 2.0M | 110‡ | — | no | no |
|
| 115 |
+
| Loom Swift Micro (original) | 1.16M | 110‡ | — | no | no |
|
| 116 |
+
| Loom Swift Mini 2 | 2.0M | 120 | not measured | barely (12% on 200–1,500-token passages) | yes |
|
| 117 |
+
| **Loom Swift Micro 2** | **1.17M** | **110** | not measured | barely (7%) | **yes** |
|
| 118 |
+
|
| 119 |
+
† scored on a battery with every test prompt scrubbed from training. ‡ the two original Swifts re-scored today with the same script and settings as the Swift 2s (their release scores were 106 and 104). Earlier rows are each model's release score. Never compare scores from different command lines.
|
| 120 |
+
|
| 121 |
+
## Read this before you use it
|
| 122 |
+
|
| 123 |
+
Every point was measured on our own held-out tests.
|
| 124 |
+
|
| 125 |
+
- **It is weaker than Swift Mini 2 at almost everything.** Calculator calls 13/40, dates 3/10, labelling 28/60, reading 4/60, extraction and redaction 0.
|
| 126 |
+
- **On the acceptance battery it ties the original (110/133) and is worse in two places**: attribution after a lookup went from 5/5 to 0/5 and admitting an unknowable from 7/8 to 5/8. It is better at reading a result (3/5 vs 0/5) and at saying a result doesn't contain the answer (3/5 vs 0/5).
|
| 127 |
+
- **Its "(sure)" is nearly meaningless**: right 22% of the time it says it.
|
| 128 |
+
- **Do not use it for calculation, extraction or redaction.** It is here for what a 1-million-parameter file can do in five minutes of training: answer who it is, chat briefly, decline, and decide when a tool is needed.
|
| 129 |
+
- Replies are one or two short sentences. 2,048 tokens of context.
|
| 130 |
+
|
| 131 |
+
## Usage — phone and desktop chat apps (PocketPal, LM Studio, Jan)
|
| 132 |
+
|
| 133 |
+
Download `loom-swift-micro-2-q4_k_m.gguf` (1.2 MB) or `loom-swift-micro-2-q8_0.gguf` (1.4 MB) and load it. The chat template and the stop token are inside the file.
|
| 134 |
+
Set **repeat penalty 1.0**. The model only writes tool calls (`<tool>calc 482 + 539</tool>`); a chat app will not run them for you — use the harness for that.
|
| 135 |
+
|
| 136 |
+
## Usage — the harness (search, calculator, date)
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
python3 harness.py "whats the capital of peru"
|
| 140 |
+
python3 harness.py "what is 482 + 539"
|
| 141 |
+
python3 harness.py --show "who wrote hamlet" # see what it searched and read
|
| 142 |
+
python3 harness.py --no-tools "who are you"
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
Stdlib only; Wikipedia needs no key. Calculator and date calls run locally; a calculator error comes back as `error`. **Never feed a failed lookup back as a result** — the model answers from the error text.
|
| 146 |
+
|
| 147 |
+
## Usage — Ollama / llama.cpp
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
ollama run hf.co/textilelabs/Loom-Swift-Micro-2 "who are you"
|
| 151 |
+
llama-cli -m loom-swift-micro-2-q8_0.gguf --jinja -cnv # uses the template in the GGUF
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
`template` and `params` are read automatically by Ollama. **Do not add a repetition penalty.**
|
| 155 |
+
|
| 156 |
+
## Usage — transformers (4.50 or newer)
|
| 157 |
+
|
| 158 |
+
```python
|
| 159 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 160 |
+
tok = AutoTokenizer.from_pretrained("textilelabs/Loom-Swift-Micro-2")
|
| 161 |
+
model = AutoModelForCausalLM.from_pretrained("textilelabs/Loom-Swift-Micro-2").eval()
|
| 162 |
+
prompt = tok.apply_chat_template([{"role": "user", "content": "who are you"}], tokenize=False, add_generation_prompt=True)
|
| 163 |
+
ids = tok(prompt, return_tensors="pt", add_special_tokens=False).input_ids
|
| 164 |
+
out = model.generate(ids, max_new_tokens=120, do_sample=False)
|
| 165 |
+
print(tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True))
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
Raw format (tools on or off): `<tools:off>\n<user>\n{message}\n<|eot|>\n<loom>\n`.
|
| 169 |
+
|
| 170 |
+
## How it was built
|
| 171 |
+
|
| 172 |
+
| | |
|
| 173 |
+
|---|---|
|
| 174 |
+
| architecture | Gemma-3 layout: 12 layers × 96d (10 sliding-window 512 + 2 global), 2 query heads / 1 KV head of 32, GeGLU 160, tied embeddings, QK-norm |
|
| 175 |
+
| parameters | 1,172,832 |
|
| 176 |
+
| vocabulary | 4,096 byte-level BPE, single digits, Qwen2-style pre-tokenizer (identical tokens in llama.cpp) |
|
| 177 |
+
| training | 5 minutes from random init, 608 steps, 40M tokens at 2,048 context; a second, focused phase for the last third |
|
| 178 |
+
| optimiser | Muon on 2-D hidden matrices, AdamW on embeddings and norms; per-conversation attention masking |
|
| 179 |
+
| attention check | before the run, the trainer's attention is checked against the reference implementation (loss and gradients identical) |
|
| 180 |
+
| hardware | Kaggle, 2 × NVIDIA T4 |
|
| 181 |
+
|
| 182 |
+
## Files
|
| 183 |
+
|
| 184 |
+
```
|
| 185 |
+
model.safetensors / config.json the model (transformers)
|
| 186 |
+
tokenizer.json / tokenizer_config.json tokenizer + chat template
|
| 187 |
+
loom-swift-micro-2-f16.gguf full precision GGUF (2.5 MB)
|
| 188 |
+
loom-swift-micro-2-q8_0.gguf / -q4_k_m.gguf phone builds (1.4 MB / 1.2 MB)
|
| 189 |
+
template / params / Modelfile Ollama
|
| 190 |
+
harness.py runnable harness — search, calculator, date; stdlib only
|
| 191 |
+
ATTRIBUTION.md required credits for the training data
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## License
|
| 195 |
+
|
| 196 |
+
Model: MIT. Training data keeps its original licences — see ATTRIBUTION.md.
|
banner.jpeg
ADDED
|
Git LFS Details
|
config.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_sliding_window_pattern": 6,
|
| 3 |
+
"architectures": [
|
| 4 |
+
"Gemma3ForCausalLM"
|
| 5 |
+
],
|
| 6 |
+
"attention_bias": false,
|
| 7 |
+
"attention_dropout": 0.0,
|
| 8 |
+
"attn_logit_softcapping": null,
|
| 9 |
+
"bos_token_id": null,
|
| 10 |
+
"dtype": "float32",
|
| 11 |
+
"eos_token_id": 0,
|
| 12 |
+
"final_logit_softcapping": null,
|
| 13 |
+
"head_dim": 32,
|
| 14 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 15 |
+
"hidden_size": 96,
|
| 16 |
+
"initializer_range": 0.02,
|
| 17 |
+
"intermediate_size": 160,
|
| 18 |
+
"layer_types": [
|
| 19 |
+
"sliding_attention",
|
| 20 |
+
"sliding_attention",
|
| 21 |
+
"sliding_attention",
|
| 22 |
+
"sliding_attention",
|
| 23 |
+
"sliding_attention",
|
| 24 |
+
"full_attention",
|
| 25 |
+
"sliding_attention",
|
| 26 |
+
"sliding_attention",
|
| 27 |
+
"sliding_attention",
|
| 28 |
+
"sliding_attention",
|
| 29 |
+
"sliding_attention",
|
| 30 |
+
"full_attention"
|
| 31 |
+
],
|
| 32 |
+
"max_position_embeddings": 2048,
|
| 33 |
+
"model_type": "gemma3_text",
|
| 34 |
+
"num_attention_heads": 2,
|
| 35 |
+
"num_hidden_layers": 12,
|
| 36 |
+
"num_key_value_heads": 1,
|
| 37 |
+
"pad_token_id": 1,
|
| 38 |
+
"query_pre_attn_scalar": 32,
|
| 39 |
+
"rms_norm_eps": 1e-06,
|
| 40 |
+
"rope_parameters": {
|
| 41 |
+
"full_attention": {
|
| 42 |
+
"rope_theta": 1000000.0,
|
| 43 |
+
"rope_type": "default"
|
| 44 |
+
},
|
| 45 |
+
"sliding_attention": {
|
| 46 |
+
"rope_theta": 10000.0,
|
| 47 |
+
"rope_type": "default"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"sliding_window": 512,
|
| 51 |
+
"tie_word_embeddings": true,
|
| 52 |
+
"transformers_version": "5.16.1",
|
| 53 |
+
"use_bidirectional_attention": false,
|
| 54 |
+
"use_cache": true,
|
| 55 |
+
"vocab_size": 4096
|
| 56 |
+
}
|
generation_config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"eos_token_id": 0,
|
| 4 |
+
"output_attentions": false,
|
| 5 |
+
"output_hidden_states": false,
|
| 6 |
+
"pad_token_id": 1,
|
| 7 |
+
"transformers_version": "5.16.1",
|
| 8 |
+
"use_cache": false
|
| 9 |
+
}
|
harness.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Loom Swift 2 harness — search, calculator and date for Loom Swift Mini 2 / Micro 2.
|
| 3 |
+
|
| 4 |
+
The model never searches or calculates. It decides a tool is needed and writes the call:
|
| 5 |
+
|
| 6 |
+
<lookup>france</lookup> -> this script searches Wikipedia and hands back <result>
|
| 7 |
+
<tool>calc 482 + 539</tool> -> this script runs the calculator and hands back <output>
|
| 8 |
+
<tool>date +20d</tool> -> this script reads your clock and hands back <output>
|
| 9 |
+
(calc errors — malformed, unsafe, divide by zero — come back as "error"; a long search result is cut to fit the 2,048-token context)
|
| 10 |
+
|
| 11 |
+
This script does the rest: searches Wikipedia, finds the ONE sentence most likely to
|
| 12 |
+
hold the answer, hands it back as a <result>, and lets the model answer from it.
|
| 13 |
+
|
| 14 |
+
python3 harness.py "what's the capital of france"
|
| 15 |
+
python3 harness.py # interactive
|
| 16 |
+
python3 harness.py --no-tools "who are you"
|
| 17 |
+
python3 harness.py --show "who wrote hamlet" # print what was searched and read
|
| 18 |
+
|
| 19 |
+
How it finds the answer, and why each step exists (all measured on live questions):
|
| 20 |
+
* searches the model's query AND the subject it can see in your question —
|
| 21 |
+
"whats the capital of france" searched as-is returns "Capital city" and "Das Kapital"
|
| 22 |
+
* prefers the real article over lists, films, albums and disambiguation pages
|
| 23 |
+
* reads the article's intro first, and further only when the intro has no answer of
|
| 24 |
+
the right kind (a height with a unit, a year, a number, a name)
|
| 25 |
+
* strips brackets and pronunciation guides, so real text looks like training text
|
| 26 |
+
* hands back ONE sentence. A 340-character window found the answer more often but the
|
| 27 |
+
model misread it four times in five; one sentence doubled the final score (15% -> 30%)
|
| 28 |
+
|
| 29 |
+
Swap search() for anything you like — the contract is text in, one sentence out.
|
| 30 |
+
Wikipedia needs no API key. Stdlib only.
|
| 31 |
+
"""
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import argparse, ast, datetime, json, operator, re, ssl, sys, time, urllib.error, urllib.parse, urllib.request
|
| 35 |
+
|
| 36 |
+
try: # macOS system Python often lacks a CA bundle
|
| 37 |
+
import certifi
|
| 38 |
+
SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
| 39 |
+
except Exception:
|
| 40 |
+
SSL_CTX = ssl.create_default_context()
|
| 41 |
+
|
| 42 |
+
# ------------------------------------------------------------------ calculator + date (stdlib; identical to the functions the model was trained against)
|
| 43 |
+
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, ast.Mod: operator.mod}
|
| 44 |
+
def calc_eval(expr: str) -> str:
|
| 45 |
+
def ev(n):
|
| 46 |
+
if isinstance(n, ast.Expression): return ev(n.body)
|
| 47 |
+
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return n.value
|
| 48 |
+
if isinstance(n, ast.BinOp) and type(n.op) in _OPS:
|
| 49 |
+
if isinstance(n.op, ast.Pow) and abs(ev(n.right)) > 12: raise ValueError("power too large")
|
| 50 |
+
return _OPS[type(n.op)](ev(n.left), ev(n.right))
|
| 51 |
+
if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS: return _OPS[type(n.op)](ev(n.operand))
|
| 52 |
+
raise ValueError(expr)
|
| 53 |
+
v = ev(ast.parse(expr.strip(), mode="eval")); return str(int(v)) if float(v).is_integer() else f"{v:.6g}"
|
| 54 |
+
|
| 55 |
+
def safe_calc(expr: str) -> str:
|
| 56 |
+
"""Any failure — malformed, unsafe, divide by zero, too large — is the word 'error', never a crash and never a made-up number."""
|
| 57 |
+
try: return calc_eval(expr)
|
| 58 |
+
except Exception: return "error"
|
| 59 |
+
|
| 60 |
+
def today() -> datetime.date: return datetime.date.today()
|
| 61 |
+
|
| 62 |
+
def date_eval(cmd: str, day: datetime.date) -> str:
|
| 63 |
+
"""cmd: 'today' | '+Nd' | '-Nd' | 'YYYY-MM-DD to YYYY-MM-DD' -> '2026-11-14 (Saturday)' or '45 days'."""
|
| 64 |
+
cmd = cmd.strip()
|
| 65 |
+
if cmd == "today": d = day
|
| 66 |
+
elif m := re.fullmatch(r"([+-])(\d+)d", cmd): d = day + datetime.timedelta(days=int(m[2]) * (1 if m[1] == "+" else -1))
|
| 67 |
+
elif m := re.fullmatch(r"(\d{4}-\d{2}-\d{2}) to (\d{4}-\d{2}-\d{2})", cmd):
|
| 68 |
+
a, b = datetime.date.fromisoformat(m[1]), datetime.date.fromisoformat(m[2]); return f"{abs((b - a).days)} days"
|
| 69 |
+
else: raise ValueError(cmd)
|
| 70 |
+
return f"{d.isoformat()} ({d.strftime('%A')})"
|
| 71 |
+
|
| 72 |
+
def safe_date(cmd: str) -> str:
|
| 73 |
+
try: return date_eval(cmd, today())
|
| 74 |
+
except Exception: return "error"
|
| 75 |
+
|
| 76 |
+
TOOL = re.compile(r"<tool>\s*(calc|date)\s+(.*?)</tool>", re.S)
|
| 77 |
+
|
| 78 |
+
# ------------------------------------------------------------------ context cap
|
| 79 |
+
MAX_RESULT_TOKENS = 1200 # the model reads up to ~1,500 tokens of result; 2,048 minus the question, the reply and a margin
|
| 80 |
+
|
| 81 |
+
def fit_result(text: str, tok_budget: int = MAX_RESULT_TOKENS) -> tuple:
|
| 82 |
+
"""Cut a retrieved passage at a sentence boundary so it fits the context (~3.3 characters per token). Returns (text, truncated).
|
| 83 |
+
No marker is added to the text: the model was never trained on one, and an unseen marker would be read as content."""
|
| 84 |
+
cap = int(tok_budget * 3.3)
|
| 85 |
+
if len(text) <= cap: return text, False
|
| 86 |
+
out = ""
|
| 87 |
+
for s in re.split(r"(?<=[.!?]) +", text):
|
| 88 |
+
if len(out) + len(s) + 1 > cap: break
|
| 89 |
+
out = (out + " " + s).strip()
|
| 90 |
+
return (out or text[:cap]), True
|
| 91 |
+
|
| 92 |
+
OLLAMA = "http://localhost:11434/api/generate"
|
| 93 |
+
MODEL = "hf.co/textilelabs/Loom-Swift-Micro-2"
|
| 94 |
+
API = "https://en.wikipedia.org/w/api.php?"
|
| 95 |
+
# Wikipedia returns 403 without a descriptive User-Agent.
|
| 96 |
+
UA = {"User-Agent": "LoomHarness/3.0 (Textile Labs; https://huggingface.co/textilelabs)"}
|
| 97 |
+
LOOKUP = re.compile(r"<lookup>(.*?)</lookup>", re.S)
|
| 98 |
+
_cache: dict = {}
|
| 99 |
+
|
| 100 |
+
# ------------------------------------------------------------------- the model
|
| 101 |
+
def loom(prompt: str, n: int = 64) -> str:
|
| 102 |
+
body = json.dumps({"model": MODEL, "prompt": prompt, "raw": True, "stream": False,
|
| 103 |
+
"options": {"temperature": 0, "num_predict": max(n, 160), "repeat_penalty": 1.0, "num_ctx": 2048,
|
| 104 |
+
"stop": ["<|eot|>", "<user>", "<result>", "<output>"]}}).encode()
|
| 105 |
+
req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"})
|
| 106 |
+
with urllib.request.urlopen(req, timeout=120) as r:
|
| 107 |
+
return json.load(r)["response"].strip()
|
| 108 |
+
|
| 109 |
+
# ------------------------------------------------------------------ wikipedia
|
| 110 |
+
def _get(params: dict) -> dict:
|
| 111 |
+
key = json.dumps(params, sort_keys=True)
|
| 112 |
+
if key in _cache:
|
| 113 |
+
return _cache[key]
|
| 114 |
+
for attempt in range(3):
|
| 115 |
+
try:
|
| 116 |
+
with urllib.request.urlopen(urllib.request.Request(
|
| 117 |
+
API + urllib.parse.urlencode(params), headers=UA),
|
| 118 |
+
context=SSL_CTX, timeout=20) as r:
|
| 119 |
+
_cache[key] = json.load(r)
|
| 120 |
+
return _cache[key]
|
| 121 |
+
except urllib.error.HTTPError as e:
|
| 122 |
+
if e.code in (429, 502, 503, 504):
|
| 123 |
+
time.sleep(3 * (attempt + 1)); continue
|
| 124 |
+
raise
|
| 125 |
+
raise RuntimeError("Wikipedia busy or rate-limited — try again")
|
| 126 |
+
|
| 127 |
+
def search(q: str, n: int = 3) -> list:
|
| 128 |
+
return [h["title"] for h in _get({"action": "query", "list": "search", "srsearch": q,
|
| 129 |
+
"format": "json", "srlimit": n})["query"]["search"]]
|
| 130 |
+
|
| 131 |
+
def _extract(title: str, intro: bool) -> str:
|
| 132 |
+
p = {"action": "query", "prop": "extracts", "explaintext": 1, "titles": title,
|
| 133 |
+
"format": "json", "redirects": 1}
|
| 134 |
+
if intro:
|
| 135 |
+
p["exintro"] = 1
|
| 136 |
+
return next(iter(_get(p)["query"]["pages"].values())).get("extract", "") or ""
|
| 137 |
+
|
| 138 |
+
# ------------------------------------------------------------------ the finder
|
| 139 |
+
SENT = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9])")
|
| 140 |
+
PAREN = re.compile(r"\s*\([^()]*\)")
|
| 141 |
+
HEADING = re.compile(r"^\s*=+[^=]+=+\s*$", re.M)
|
| 142 |
+
STOP = set(("what whats who whos whom whose when where which why how is are was were be the a an "
|
| 143 |
+
"of in on to for does did do by from with as at and or that this it its there tell me "
|
| 144 |
+
"please can you many much").split())
|
| 145 |
+
ATTR = set(("capital city height tall high elevation population largest biggest smallest longest "
|
| 146 |
+
"shortest tallest highest deepest first last symbol chemical language languages spoken "
|
| 147 |
+
"legs year date end ended sink sank invented inventor discovered discovery developed "
|
| 148 |
+
"wrote written author painted painter president founded born died age old size area "
|
| 149 |
+
"distance speed").split())
|
| 150 |
+
JUNK = re.compile(r"^(lists? of|outline of|index of|timeline of)\b|\((film|album|song|band|"
|
| 151 |
+
r"novel|play|tv series|musical|opera|video game|book|composition|poem)\)|"
|
| 152 |
+
r"\bdisambiguation\b", re.I)
|
| 153 |
+
|
| 154 |
+
def keywords(t: str) -> list:
|
| 155 |
+
return [w for w in re.findall(r"[^\W_]+", t.lower()) if w not in STOP]
|
| 156 |
+
|
| 157 |
+
def subject(question: str) -> str:
|
| 158 |
+
kw = keywords(question)
|
| 159 |
+
return " ".join(k for k in kw if k not in ATTR) or " ".join(kw)
|
| 160 |
+
|
| 161 |
+
def clean(t: str) -> str:
|
| 162 |
+
prev = None
|
| 163 |
+
while prev != t:
|
| 164 |
+
prev, t = t, PAREN.sub("", t)
|
| 165 |
+
return re.sub(r"\s+", " ", t.replace(" ,", ",")).strip()
|
| 166 |
+
|
| 167 |
+
def _hard(q: str, s: str) -> float:
|
| 168 |
+
"""The answer is of the right KIND: a height with a unit, a year, a number, a name."""
|
| 169 |
+
b = 0.0
|
| 170 |
+
if re.search(r"\b(how tall|how high|height|elevation)\b", q):
|
| 171 |
+
b += 2.0 if re.search(r"\d[\d,.]*\s*(m|metres|meters|ft|feet|km)\b", s) else 0
|
| 172 |
+
if re.search(r"\b(when|what year|which year|what date)\b", q):
|
| 173 |
+
b += 2.0 if re.search(r"\b(1\d{3}|20\d{2})\b", s) else 0
|
| 174 |
+
if re.search(r"\b(how many|how much|population|number of)\b", q):
|
| 175 |
+
b += 1.5 if re.search(r"\d", s) else 0
|
| 176 |
+
if re.search(r"\bwho\b", q):
|
| 177 |
+
b += 1.5 if re.search(r"\b[A-Z][a-z]+ [A-Z][a-z]+", s) else 0
|
| 178 |
+
if re.search(r"\bsymbol\b", q):
|
| 179 |
+
b += 2.0 if re.search(r"\bsymbol\b", s, re.I) else 0
|
| 180 |
+
if re.search(r"\bcapital\b", q):
|
| 181 |
+
b += 2.0 if re.search(r"\bcapital\b", s, re.I) else 0
|
| 182 |
+
return b
|
| 183 |
+
|
| 184 |
+
def _kind(q: str, s: str) -> float:
|
| 185 |
+
b, sl = _hard(q, s), s.lower()
|
| 186 |
+
if re.search(r"\b(how tall|how high|height|elevation)\b", q):
|
| 187 |
+
b += 1.5 if re.search(r"\b(summit|elevation|height|above sea level|highest|stands)\b", sl) else -0.5
|
| 188 |
+
if re.search(r"\b(end|ended|finish|finished)\b", q):
|
| 189 |
+
b += 1.5 if re.search(r"\b(ended|end of|surrender|surrendered|concluded|finished)\b", sl) else -0.5
|
| 190 |
+
if re.search(r"\bpopulation\b", q):
|
| 191 |
+
b += 2.0 if re.search(r"\d{1,3}(,\d{3})+|\d+(\.\d+)?\s*(million|billion)", s) else -1.0
|
| 192 |
+
if re.search(r"\b(invent|invented|inventor|discovered|wrote|painted|composed|founded)\b", q):
|
| 193 |
+
b += 1.0 if re.search(r"\b[A-Z][a-z]+ (?:[A-Z][a-z]+ )?[A-Z][a-z]+\b", s) else 0.0
|
| 194 |
+
return b
|
| 195 |
+
|
| 196 |
+
def find(query: str, question: str) -> tuple:
|
| 197 |
+
"""One sentence most likely to hold the answer, and the article it came from."""
|
| 198 |
+
q = question.lower()
|
| 199 |
+
subj = subject(question)
|
| 200 |
+
pool = {}
|
| 201 |
+
for tq in dict.fromkeys(x for x in (query.strip(), subj, " ".join(keywords(question))) if x):
|
| 202 |
+
for rank, t in enumerate(search(tq, 3)):
|
| 203 |
+
tl = t.lower()
|
| 204 |
+
s = (4.0 if tl in (subj, query.strip().lower()) else 2.0 if subj and tl.startswith(subj) else 0.0)
|
| 205 |
+
s += -4.0 if JUNK.search(t) else 0.0
|
| 206 |
+
pool[t] = max(pool.get(t, -1e9), s - 0.3 * rank)
|
| 207 |
+
qk = list(dict.fromkeys(keywords(question) + keywords(query)))
|
| 208 |
+
top = sorted(pool.items(), key=lambda x: -x[1])[:3]
|
| 209 |
+
typed = bool(re.search(r"\b(how tall|how high|height|elevation|when|what year|which year|"
|
| 210 |
+
r"how many|how much|population|who|symbol|capital)\b", q))
|
| 211 |
+
best = (-1e9, "", "")
|
| 212 |
+
for intro in (True, False):
|
| 213 |
+
found_kind = False
|
| 214 |
+
for title, ps in top:
|
| 215 |
+
raw = _extract(title, intro)
|
| 216 |
+
if re.search(r"\b(may|can) refer to\b", raw[:400]):
|
| 217 |
+
continue
|
| 218 |
+
body = clean(HEADING.sub(" ", raw))
|
| 219 |
+
sents = [s.strip() for s in SENT.split(body) if 20 < len(s.strip()) < 600]
|
| 220 |
+
for i, s in enumerate(sents[: 14 if intro else 90]):
|
| 221 |
+
sc = ps + sum(1.0 for k in qk if k in s.lower()) + _kind(q, s) + (0.5 if i < 3 else 0.0)
|
| 222 |
+
if sc > best[0]:
|
| 223 |
+
best = (sc, s, title)
|
| 224 |
+
found_kind = _hard(q, s) > 0
|
| 225 |
+
if best[1] and (not typed or found_kind):
|
| 226 |
+
break # the intro held an answer of the right kind
|
| 227 |
+
return best[1], best[2]
|
| 228 |
+
|
| 229 |
+
# ------------------------------------------------------------------ the loop
|
| 230 |
+
def ask(message: str, tools: bool = True, show: bool = False) -> str:
|
| 231 |
+
convo = f"<tools:{'on' if tools else 'off'}>\n<user>\n{message.strip()}\n<|eot|>\n<loom>\n"
|
| 232 |
+
first = loom(convo)
|
| 233 |
+
t = TOOL.search(first)
|
| 234 |
+
if t: # calculator / date: run it, hand back the output
|
| 235 |
+
kind, arg = t.group(1), t.group(2).strip(); out = safe_calc(arg) if kind == "calc" else safe_date(arg)
|
| 236 |
+
if show: print(f" [{kind}: {arg!r} -> {out}]")
|
| 237 |
+
return loom(convo + first + f"<|eot|>\n<output>\n{out}\n<|eot|>\n<loom>\n", n=64)
|
| 238 |
+
m = LOOKUP.search(first)
|
| 239 |
+
if not m:
|
| 240 |
+
return first
|
| 241 |
+
query = m.group(1).strip()
|
| 242 |
+
try:
|
| 243 |
+
result, source = find(query, message)
|
| 244 |
+
except Exception as e:
|
| 245 |
+
# Never feed an error in as if it were a result — the model will answer from it.
|
| 246 |
+
return f"[harness] lookup failed for {query!r}: {e}"
|
| 247 |
+
if not result:
|
| 248 |
+
return f"[harness] nothing found for {query!r}"
|
| 249 |
+
result, cut = fit_result(result)
|
| 250 |
+
if show:
|
| 251 |
+
print(f" [searched: {query!r}]\n [read from {source}: {result[:150]}]" + ("\n [harness] result shortened to fit the context" if cut else ""))
|
| 252 |
+
return loom(convo + first + f"<|eot|>\n<result>\n{result}\n<|eot|>\n<loom>\n", n=64)
|
| 253 |
+
|
| 254 |
+
def main() -> int:
|
| 255 |
+
global MODEL
|
| 256 |
+
ap = argparse.ArgumentParser(description="Loom Swift 2 harness")
|
| 257 |
+
ap.add_argument("message", nargs="*")
|
| 258 |
+
ap.add_argument("--no-tools", action="store_true", help="chat only, no lookups")
|
| 259 |
+
ap.add_argument("--show", action="store_true", help="print the query and the sentence read")
|
| 260 |
+
ap.add_argument("--model", default=MODEL)
|
| 261 |
+
a = ap.parse_args()
|
| 262 |
+
MODEL = a.model
|
| 263 |
+
if a.message:
|
| 264 |
+
print(ask(" ".join(a.message), not a.no_tools, a.show)); return 0
|
| 265 |
+
print(f"Loom Swift harness — {MODEL} (tools {'off' if a.no_tools else 'on'}, ctrl-c to quit)\n")
|
| 266 |
+
while True:
|
| 267 |
+
try:
|
| 268 |
+
msg = input("you > ").strip()
|
| 269 |
+
except (EOFError, KeyboardInterrupt):
|
| 270 |
+
print(); return 0
|
| 271 |
+
if msg:
|
| 272 |
+
print(f"loom > {ask(msg, not a.no_tools, a.show)}\n")
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
sys.exit(main())
|
logo.jpeg
ADDED
|
Git LFS Details
|
loom-swift-micro-2-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1521a5fee2bb983315afa454c202d08dfab077776b64d3129180056880dc6123
|
| 3 |
+
size 2493984
|
loom-swift-micro-2-q4_k_m.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:53a92b7471f80ac99f79b9192df478dffd6b1de4c42e87b84cb6cc87a49d243c
|
| 3 |
+
size 1150752
|
loom-swift-micro-2-q8_0.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:582de9965742c6b78002cd64c7991c07f9f801fbfb99c80bbf180fccb6e6f4cb
|
| 3 |
+
size 1399584
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5428f86cab43f08e8b45e2c6ddeb6876f1ebe8eda93f764295151d4ba905b76f
|
| 3 |
+
size 4708104
|
params
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"stop": ["<|eot|>", "<user>", "<result>", "<output>"],
|
| 3 |
+
"temperature": 0.7,
|
| 4 |
+
"top_k": 40,
|
| 5 |
+
"repeat_penalty": 1.0,
|
| 6 |
+
"num_predict": 200,
|
| 7 |
+
"num_ctx": 2048
|
| 8 |
+
}
|
special_tokens_map.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"eos_token": "<|eot|>",
|
| 3 |
+
"pad_token": "<|pad|>",
|
| 4 |
+
"additional_special_tokens": [
|
| 5 |
+
"<tools:off>",
|
| 6 |
+
"<tools:on>",
|
| 7 |
+
"<user>",
|
| 8 |
+
"<loom>",
|
| 9 |
+
"<think>",
|
| 10 |
+
"</think>",
|
| 11 |
+
"<recall>",
|
| 12 |
+
"</recall>",
|
| 13 |
+
"<memory>",
|
| 14 |
+
"<lookup>",
|
| 15 |
+
"</lookup>",
|
| 16 |
+
"<result>",
|
| 17 |
+
"<tool>",
|
| 18 |
+
"</tool>",
|
| 19 |
+
"<output>",
|
| 20 |
+
"<doc>"
|
| 21 |
+
]
|
| 22 |
+
}
|
template
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<tools:off>
|
| 2 |
+
<user>
|
| 3 |
+
{{ .Prompt }}
|
| 4 |
+
<|eot|>
|
| 5 |
+
<loom>
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tokenizer_class": "PreTrainedTokenizerFast",
|
| 3 |
+
"add_bos_token": false,
|
| 4 |
+
"add_eos_token": false,
|
| 5 |
+
"bos_token": null,
|
| 6 |
+
"eos_token": "<|eot|>",
|
| 7 |
+
"pad_token": "<|pad|>",
|
| 8 |
+
"unk_token": null,
|
| 9 |
+
"model_max_length": 131072,
|
| 10 |
+
"clean_up_tokenization_spaces": false
|
| 11 |
+
}
|