imprint-layer 0.3.0__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.
- imprint_layer-0.3.0/LICENSE +21 -0
- imprint_layer-0.3.0/MANIFEST.in +4 -0
- imprint_layer-0.3.0/PKG-INFO +127 -0
- imprint_layer-0.3.0/README.md +109 -0
- imprint_layer-0.3.0/examples/frontier_api.py +50 -0
- imprint_layer-0.3.0/examples/local_chat.py +62 -0
- imprint_layer-0.3.0/examples/sample.profile.json +102 -0
- imprint_layer-0.3.0/pyproject.toml +32 -0
- imprint_layer-0.3.0/setup.cfg +4 -0
- imprint_layer-0.3.0/src/imprint/__init__.py +14 -0
- imprint_layer-0.3.0/src/imprint/core.py +322 -0
- imprint_layer-0.3.0/src/imprint/dashboard.py +85 -0
- imprint_layer-0.3.0/src/imprint/embedders.py +43 -0
- imprint_layer-0.3.0/src/imprint/rules.py +242 -0
- imprint_layer-0.3.0/src/imprint/steering.py +187 -0
- imprint_layer-0.3.0/src/imprint/traits.py +207 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/PKG-INFO +127 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/SOURCES.txt +23 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/dependency_links.txt +1 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/entry_points.txt +2 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/requires.txt +7 -0
- imprint_layer-0.3.0/src/imprint_layer.egg-info/top_level.txt +1 -0
- imprint_layer-0.3.0/tests/conftest.py +41 -0
- imprint_layer-0.3.0/tests/test_imprint.py +318 -0
- imprint_layer-0.3.0/tests/test_minilm_integration.py +46 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabe Hernandez
|
|
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.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imprint-layer
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A personality calibration layer for any chat model: learns how one user wants an assistant to talk, from their own messages.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: llm,personalization,system-prompt,assistant,embeddings
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy>=1.24
|
|
13
|
+
Provides-Extra: minilm
|
|
14
|
+
Requires-Dist: sentence-transformers>=2.2; extra == "minilm"
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Imprint
|
|
20
|
+
|
|
21
|
+
**Tell your bot to be wittier. It becomes wittier.**
|
|
22
|
+
|
|
23
|
+
A personality calibration layer for any chat model. Imprint learns how one person wants an assistant to talk from that person's own messages. It then renders a short plain-text block that you put in the system prompt.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from imprint import Imprint
|
|
27
|
+
|
|
28
|
+
imp = Imprint("me.profile.json")
|
|
29
|
+
|
|
30
|
+
def on_user_message(text, sent_at):
|
|
31
|
+
imp.learn(text, timestamp=sent_at) # learn first...
|
|
32
|
+
system = BASE_PROMPT + "\n\n" + imp.directive() # ...then build the prompt
|
|
33
|
+
return call_your_model(system, history)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What it learns
|
|
37
|
+
|
|
38
|
+
**Eight dials, each a value from 0 to 1:**
|
|
39
|
+
|
|
40
|
+
| Dial | Low ↔ high |
|
|
41
|
+
|---|---|
|
|
42
|
+
| verbosity | concise ↔ detailed |
|
|
43
|
+
| autonomy | ask first ↔ act on own judgment |
|
|
44
|
+
| formality | casual ↔ formal |
|
|
45
|
+
| proactivity | wait for instructions ↔ suggest next steps |
|
|
46
|
+
| riskTolerance | careful claims ↔ committed takes |
|
|
47
|
+
| humor | earnest ↔ dry, sarcastic wit |
|
|
48
|
+
| warmth | matter-of-fact ↔ emotionally present |
|
|
49
|
+
| flirt | platonic ↔ playful romantic charge |
|
|
50
|
+
|
|
51
|
+
The model never sees the numbers. Each dial has a few bands, and the directive carries one plain sentence per dial for the band it's in. Use `Imprint(traits=[...])` to learn and inject only the dials you want. A work assistant would probably drop `flirt`.
|
|
52
|
+
|
|
53
|
+
**Dials move three ways:**
|
|
54
|
+
|
|
55
|
+
- **Direct steering.** "be wittier", "more sarcastic", "less detail please", "you're too formal", "dial up the warmth", "no more snark". The named dial moves one full band, so the very next reply changes. Saying it again moves one more band.
|
|
56
|
+
- Questions about a dial ("why so sarcastic?") don't steer.
|
|
57
|
+
- Passing mentions don't steer, and neither does talk about other people ("my boss could be funnier").
|
|
58
|
+
- Praise never steers down: "you're too kind" is a compliment.
|
|
59
|
+
- A command you take back in the same message ("be wittier. actually, never mind") is cancelled.
|
|
60
|
+
- **Tone of feedback.** Messages like "you're repeating yourself, too much filler" nudge a dial a little. Each message's embedding is compared against example phrases for each end of each dial. Ordinary conversation stays under the thresholds and moves nothing.
|
|
61
|
+
- **Standing rules, captured in two tiers.** Nobody has to learn a syntax.
|
|
62
|
+
- *Explicit forms* become rules immediately: "never use emoji", "always: cite sources", "from now on, use metric units", "remember: I'm vegetarian".
|
|
63
|
+
- *Natural preference statements* become candidate rules: "I prefer metric units", "I hate it when you use bullet points". A candidate becomes a rule only when the user says the same thing again in a different message at least an hour later. Candidates are never injected, and they expire after 90 days without corroboration.
|
|
64
|
+
- Neither tier stores one-off tasks. A deadline, a named recipient, "now", or "this one" marks a task, not a preference ("always send the report to Dana by Friday" is a task).
|
|
65
|
+
|
|
66
|
+
## Model-agnostic, with an honest caveat
|
|
67
|
+
|
|
68
|
+
Imprint never calls a chat model. It works with anything that accepts a system prompt: a local model behind llama.cpp, Ollama or vLLM, or a hosted frontier model through its API. The default embedder (`all-MiniLM-L6-v2`) runs locally on CPU, so the user's messages aren't sent anywhere for learning.
|
|
69
|
+
|
|
70
|
+
**The mechanism is model-agnostic. The effect is not.** How closely replies follow the calibration block depends on how well the host model follows instructions. A strong model will follow "Dry wit and occasional sarcasm are welcome" closely. A small model may follow it loosely, or not at all.
|
|
71
|
+
|
|
72
|
+
## Integration contract
|
|
73
|
+
|
|
74
|
+
Your host provides two things:
|
|
75
|
+
|
|
76
|
+
1. **Each user message, with the time the user sent it.** Call `learn(text, timestamp=...)` before you build the prompt for that turn. Without a timestamp, Imprint still learns, but it records the time as unknown (never the processing time), skips confidence decay, and can't corroborate candidate rules. It logs a warning when this happens.
|
|
77
|
+
2. **A place to inject text into the system prompt.** `directive()` returns the block, or `""` until `warmup` interactions (default 5) have passed. It stays identical byte for byte until something is actually learned, so it works with prompt caching.
|
|
78
|
+
|
|
79
|
+
Everything beyond that is up to the host: memory, retrieval, tools, scheduling.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install imprint-layer
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The distribution is `imprint-layer`; the import is `imprint`. The default local embedder needs sentence-transformers: `pip install "imprint-layer[minilm]"`. Without it, pass your own embedder.
|
|
88
|
+
|
|
89
|
+
A custom embedder is any object with a `name` and `embed(list_of_texts) -> array of shape (n, d)`. Pass it as `Imprint(..., embedder=MyEmbedder())`.
|
|
90
|
+
|
|
91
|
+
## Examples
|
|
92
|
+
|
|
93
|
+
- `examples/local_chat.py`: a chat loop against a local OpenAI-compatible server.
|
|
94
|
+
- `examples/frontier_api.py`: the same injection, against a hosted API.
|
|
95
|
+
|
|
96
|
+
Both are reference integrations to read and adapt. They aren't products.
|
|
97
|
+
|
|
98
|
+
## Dashboard
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
imprint-dashboard my.profile.json -o dashboard.html
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This renders a read-only static page: each dial's value and band, the standing rules, candidate rules and observation counts. Try it on `examples/sample.profile.json`.
|
|
105
|
+
|
|
106
|
+
## Privacy
|
|
107
|
+
|
|
108
|
+
A profile is personal data. It records how one person talks and what they've asked for. Keep profiles on the user's machine or in your own storage, and **never commit a real profile**. The included `.gitignore` excludes `*.profile.json`. Observations store short excerpts of the messages that moved a dial, and candidate rules store the user's own sentence.
|
|
109
|
+
|
|
110
|
+
## What Imprint is not
|
|
111
|
+
|
|
112
|
+
- Not a memory system. It doesn't store facts about the user or conversation history.
|
|
113
|
+
- Not a hosted service. There's no account or server, and no data leaves your machine except through the chat calls you make yourself.
|
|
114
|
+
- Not fine-tuning. Nothing about the model changes; Imprint only adds prompt text.
|
|
115
|
+
- Not a safety layer. The calibration block is a preference, and your model's own policies still apply.
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install -e ".[test]"
|
|
121
|
+
pytest # deterministic fake embedder, no downloads
|
|
122
|
+
IMPRINT_TEST_MINILM=1 pytest # also run the real-embedder checks
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Imprint
|
|
2
|
+
|
|
3
|
+
**Tell your bot to be wittier. It becomes wittier.**
|
|
4
|
+
|
|
5
|
+
A personality calibration layer for any chat model. Imprint learns how one person wants an assistant to talk from that person's own messages. It then renders a short plain-text block that you put in the system prompt.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from imprint import Imprint
|
|
9
|
+
|
|
10
|
+
imp = Imprint("me.profile.json")
|
|
11
|
+
|
|
12
|
+
def on_user_message(text, sent_at):
|
|
13
|
+
imp.learn(text, timestamp=sent_at) # learn first...
|
|
14
|
+
system = BASE_PROMPT + "\n\n" + imp.directive() # ...then build the prompt
|
|
15
|
+
return call_your_model(system, history)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it learns
|
|
19
|
+
|
|
20
|
+
**Eight dials, each a value from 0 to 1:**
|
|
21
|
+
|
|
22
|
+
| Dial | Low ↔ high |
|
|
23
|
+
|---|---|
|
|
24
|
+
| verbosity | concise ↔ detailed |
|
|
25
|
+
| autonomy | ask first ↔ act on own judgment |
|
|
26
|
+
| formality | casual ↔ formal |
|
|
27
|
+
| proactivity | wait for instructions ↔ suggest next steps |
|
|
28
|
+
| riskTolerance | careful claims ↔ committed takes |
|
|
29
|
+
| humor | earnest ↔ dry, sarcastic wit |
|
|
30
|
+
| warmth | matter-of-fact ↔ emotionally present |
|
|
31
|
+
| flirt | platonic ↔ playful romantic charge |
|
|
32
|
+
|
|
33
|
+
The model never sees the numbers. Each dial has a few bands, and the directive carries one plain sentence per dial for the band it's in. Use `Imprint(traits=[...])` to learn and inject only the dials you want. A work assistant would probably drop `flirt`.
|
|
34
|
+
|
|
35
|
+
**Dials move three ways:**
|
|
36
|
+
|
|
37
|
+
- **Direct steering.** "be wittier", "more sarcastic", "less detail please", "you're too formal", "dial up the warmth", "no more snark". The named dial moves one full band, so the very next reply changes. Saying it again moves one more band.
|
|
38
|
+
- Questions about a dial ("why so sarcastic?") don't steer.
|
|
39
|
+
- Passing mentions don't steer, and neither does talk about other people ("my boss could be funnier").
|
|
40
|
+
- Praise never steers down: "you're too kind" is a compliment.
|
|
41
|
+
- A command you take back in the same message ("be wittier. actually, never mind") is cancelled.
|
|
42
|
+
- **Tone of feedback.** Messages like "you're repeating yourself, too much filler" nudge a dial a little. Each message's embedding is compared against example phrases for each end of each dial. Ordinary conversation stays under the thresholds and moves nothing.
|
|
43
|
+
- **Standing rules, captured in two tiers.** Nobody has to learn a syntax.
|
|
44
|
+
- *Explicit forms* become rules immediately: "never use emoji", "always: cite sources", "from now on, use metric units", "remember: I'm vegetarian".
|
|
45
|
+
- *Natural preference statements* become candidate rules: "I prefer metric units", "I hate it when you use bullet points". A candidate becomes a rule only when the user says the same thing again in a different message at least an hour later. Candidates are never injected, and they expire after 90 days without corroboration.
|
|
46
|
+
- Neither tier stores one-off tasks. A deadline, a named recipient, "now", or "this one" marks a task, not a preference ("always send the report to Dana by Friday" is a task).
|
|
47
|
+
|
|
48
|
+
## Model-agnostic, with an honest caveat
|
|
49
|
+
|
|
50
|
+
Imprint never calls a chat model. It works with anything that accepts a system prompt: a local model behind llama.cpp, Ollama or vLLM, or a hosted frontier model through its API. The default embedder (`all-MiniLM-L6-v2`) runs locally on CPU, so the user's messages aren't sent anywhere for learning.
|
|
51
|
+
|
|
52
|
+
**The mechanism is model-agnostic. The effect is not.** How closely replies follow the calibration block depends on how well the host model follows instructions. A strong model will follow "Dry wit and occasional sarcasm are welcome" closely. A small model may follow it loosely, or not at all.
|
|
53
|
+
|
|
54
|
+
## Integration contract
|
|
55
|
+
|
|
56
|
+
Your host provides two things:
|
|
57
|
+
|
|
58
|
+
1. **Each user message, with the time the user sent it.** Call `learn(text, timestamp=...)` before you build the prompt for that turn. Without a timestamp, Imprint still learns, but it records the time as unknown (never the processing time), skips confidence decay, and can't corroborate candidate rules. It logs a warning when this happens.
|
|
59
|
+
2. **A place to inject text into the system prompt.** `directive()` returns the block, or `""` until `warmup` interactions (default 5) have passed. It stays identical byte for byte until something is actually learned, so it works with prompt caching.
|
|
60
|
+
|
|
61
|
+
Everything beyond that is up to the host: memory, retrieval, tools, scheduling.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install imprint-layer
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The distribution is `imprint-layer`; the import is `imprint`. The default local embedder needs sentence-transformers: `pip install "imprint-layer[minilm]"`. Without it, pass your own embedder.
|
|
70
|
+
|
|
71
|
+
A custom embedder is any object with a `name` and `embed(list_of_texts) -> array of shape (n, d)`. Pass it as `Imprint(..., embedder=MyEmbedder())`.
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
- `examples/local_chat.py`: a chat loop against a local OpenAI-compatible server.
|
|
76
|
+
- `examples/frontier_api.py`: the same injection, against a hosted API.
|
|
77
|
+
|
|
78
|
+
Both are reference integrations to read and adapt. They aren't products.
|
|
79
|
+
|
|
80
|
+
## Dashboard
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
imprint-dashboard my.profile.json -o dashboard.html
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This renders a read-only static page: each dial's value and band, the standing rules, candidate rules and observation counts. Try it on `examples/sample.profile.json`.
|
|
87
|
+
|
|
88
|
+
## Privacy
|
|
89
|
+
|
|
90
|
+
A profile is personal data. It records how one person talks and what they've asked for. Keep profiles on the user's machine or in your own storage, and **never commit a real profile**. The included `.gitignore` excludes `*.profile.json`. Observations store short excerpts of the messages that moved a dial, and candidate rules store the user's own sentence.
|
|
91
|
+
|
|
92
|
+
## What Imprint is not
|
|
93
|
+
|
|
94
|
+
- Not a memory system. It doesn't store facts about the user or conversation history.
|
|
95
|
+
- Not a hosted service. There's no account or server, and no data leaves your machine except through the chat calls you make yourself.
|
|
96
|
+
- Not fine-tuning. Nothing about the model changes; Imprint only adds prompt text.
|
|
97
|
+
- Not a safety layer. The calibration block is a preference, and your model's own policies still apply.
|
|
98
|
+
|
|
99
|
+
## Tests
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install -e ".[test]"
|
|
103
|
+
pytest # deterministic fake embedder, no downloads
|
|
104
|
+
IMPRINT_TEST_MINILM=1 pytest # also run the real-embedder checks
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Reference integration: a hosted frontier model via an OpenAI-compatible API.
|
|
2
|
+
|
|
3
|
+
Imprint itself runs locally (the default embedder never sends text anywhere).
|
|
4
|
+
Only the chat request goes to the provider, with the calibration block in the
|
|
5
|
+
system message, same as any other system prompt text.
|
|
6
|
+
|
|
7
|
+
pip install "imprint-layer[minilm]"
|
|
8
|
+
export API_BASE_URL=https://api.example.com/v1 API_KEY=... API_MODEL=...
|
|
9
|
+
python examples/frontier_api.py "be a bit wittier. what's a good name for a cat?"
|
|
10
|
+
|
|
11
|
+
Any provider that accepts a system prompt works the same way; adapt the
|
|
12
|
+
request shape for providers with their own SDK.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.request
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
|
|
22
|
+
from imprint import Imprint
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
text = " ".join(sys.argv[1:]) or "hello"
|
|
27
|
+
imp = Imprint("me.profile.json", axes_cache=".imprint/axes.npz")
|
|
28
|
+
imp.learn(text, timestamp=datetime.now(timezone.utc))
|
|
29
|
+
block = imp.directive()
|
|
30
|
+
|
|
31
|
+
system = "You are a helpful assistant."
|
|
32
|
+
if block:
|
|
33
|
+
system += f"\n\n## Behavior calibration\n{block}"
|
|
34
|
+
body = {
|
|
35
|
+
"model": os.environ["API_MODEL"],
|
|
36
|
+
"messages": [{"role": "system", "content": system},
|
|
37
|
+
{"role": "user", "content": text}],
|
|
38
|
+
}
|
|
39
|
+
req = urllib.request.Request(
|
|
40
|
+
f"{os.environ['API_BASE_URL'].rstrip('/')}/chat/completions",
|
|
41
|
+
data=json.dumps(body).encode(),
|
|
42
|
+
headers={"Content-Type": "application/json",
|
|
43
|
+
"Authorization": f"Bearer {os.environ['API_KEY']}"},
|
|
44
|
+
)
|
|
45
|
+
with urllib.request.urlopen(req, timeout=120) as r:
|
|
46
|
+
print(json.load(r)["choices"][0]["message"]["content"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Reference integration: a local model behind an OpenAI-compatible server.
|
|
2
|
+
|
|
3
|
+
Works with llama.cpp's llama-server, Ollama, LM Studio, vLLM, and similar.
|
|
4
|
+
Per message: learn first (so a command like "be wittier" applies to this very
|
|
5
|
+
reply), then rebuild the system prompt with the Imprint block.
|
|
6
|
+
|
|
7
|
+
pip install "imprint-layer[minilm]"
|
|
8
|
+
python examples/local_chat.py --base-url http://127.0.0.1:8080/v1 --model local
|
|
9
|
+
|
|
10
|
+
This is a reference, not a product: no streaming, no history trimming.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import urllib.request
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
|
|
19
|
+
from imprint import Imprint
|
|
20
|
+
|
|
21
|
+
BASE_PROMPT = "You are a helpful assistant."
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def chat(base_url: str, model: str, messages: list[dict]) -> str:
|
|
25
|
+
req = urllib.request.Request(
|
|
26
|
+
f"{base_url.rstrip('/')}/chat/completions",
|
|
27
|
+
data=json.dumps({"model": model, "messages": messages}).encode(),
|
|
28
|
+
headers={"Content-Type": "application/json"},
|
|
29
|
+
)
|
|
30
|
+
with urllib.request.urlopen(req, timeout=300) as r:
|
|
31
|
+
return json.load(r)["choices"][0]["message"]["content"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
ap = argparse.ArgumentParser()
|
|
36
|
+
ap.add_argument("--base-url", default="http://127.0.0.1:8080/v1")
|
|
37
|
+
ap.add_argument("--model", default="local")
|
|
38
|
+
ap.add_argument("--profile", default="me.profile.json")
|
|
39
|
+
a = ap.parse_args()
|
|
40
|
+
|
|
41
|
+
imp = Imprint(a.profile, axes_cache=".imprint/axes.npz")
|
|
42
|
+
history: list[dict] = []
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
text = input("you> ").strip()
|
|
46
|
+
except EOFError:
|
|
47
|
+
break
|
|
48
|
+
if not text:
|
|
49
|
+
continue
|
|
50
|
+
# 1. Learn from the message, stamped with when the user sent it.
|
|
51
|
+
imp.learn(text, timestamp=datetime.now(timezone.utc))
|
|
52
|
+
# 2. Inject the calibration block into the system prompt.
|
|
53
|
+
block = imp.directive()
|
|
54
|
+
system = BASE_PROMPT + (f"\n\n## Behavior calibration\n{block}" if block else "")
|
|
55
|
+
history.append({"role": "user", "content": text})
|
|
56
|
+
reply = chat(a.base_url, a.model, [{"role": "system", "content": system}] + history)
|
|
57
|
+
history.append({"role": "assistant", "content": reply})
|
|
58
|
+
print(f"bot> {reply}\n")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "imprint.profile/1",
|
|
3
|
+
"created": "2026-10-10T18:21:28.469+00:00",
|
|
4
|
+
"lastUpdated": "2026-10-10T18:21:28.469+00:00",
|
|
5
|
+
"interactions": 27,
|
|
6
|
+
"traits": {
|
|
7
|
+
"verbosity": {
|
|
8
|
+
"value": 0.39,
|
|
9
|
+
"confidence": 0.232,
|
|
10
|
+
"description": "concise and direct ↔ detailed explanations"
|
|
11
|
+
},
|
|
12
|
+
"autonomy": {
|
|
13
|
+
"value": 0.5,
|
|
14
|
+
"confidence": 0.2,
|
|
15
|
+
"description": "always ask first ↔ act on own judgment"
|
|
16
|
+
},
|
|
17
|
+
"formality": {
|
|
18
|
+
"value": 0.39,
|
|
19
|
+
"confidence": 0.232,
|
|
20
|
+
"description": "casual and conversational ↔ formal and precise"
|
|
21
|
+
},
|
|
22
|
+
"proactivity": {
|
|
23
|
+
"value": 0.5,
|
|
24
|
+
"confidence": 0.2,
|
|
25
|
+
"description": "wait for instructions ↔ suggest next steps unprompted"
|
|
26
|
+
},
|
|
27
|
+
"riskTolerance": {
|
|
28
|
+
"value": 0.61,
|
|
29
|
+
"confidence": 0.23979225590616793,
|
|
30
|
+
"description": "cautious claims ↔ confident, committed takes"
|
|
31
|
+
},
|
|
32
|
+
"humor": {
|
|
33
|
+
"value": 0.56,
|
|
34
|
+
"confidence": 0.232,
|
|
35
|
+
"description": "straight and earnest ↔ dry, sarcastic wit"
|
|
36
|
+
},
|
|
37
|
+
"warmth": {
|
|
38
|
+
"value": 0.5,
|
|
39
|
+
"confidence": 0.2,
|
|
40
|
+
"description": "matter-of-fact ↔ emotionally present"
|
|
41
|
+
},
|
|
42
|
+
"flirt": {
|
|
43
|
+
"value": 0.2,
|
|
44
|
+
"confidence": 0.2,
|
|
45
|
+
"description": "platonic ↔ playful romantic charge"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"rules": [
|
|
49
|
+
"Never use emoji",
|
|
50
|
+
"Always cite sources when you state a statistic"
|
|
51
|
+
],
|
|
52
|
+
"candidateRules": [
|
|
53
|
+
{
|
|
54
|
+
"text": "I prefer metric units",
|
|
55
|
+
"polarity": 1,
|
|
56
|
+
"key": [
|
|
57
|
+
"metric",
|
|
58
|
+
"unit"
|
|
59
|
+
],
|
|
60
|
+
"evidence": [
|
|
61
|
+
"2026-10-09T12:15:00.000+00:00"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"observations": [
|
|
66
|
+
{
|
|
67
|
+
"timestamp": "2026-10-09T08:15:00.000+00:00",
|
|
68
|
+
"trait": "verbosity",
|
|
69
|
+
"direction": -1.0,
|
|
70
|
+
"source": "steer",
|
|
71
|
+
"detail": "'be more concise' 0.500→0.390"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"timestamp": "2026-10-09T09:15:00.000+00:00",
|
|
75
|
+
"trait": "humor",
|
|
76
|
+
"direction": 1.0,
|
|
77
|
+
"source": "steer",
|
|
78
|
+
"detail": "'be wittier' 0.450→0.560"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"timestamp": "2026-10-09T12:15:00.000+00:00",
|
|
82
|
+
"trait": "riskTolerance",
|
|
83
|
+
"direction": -1.0,
|
|
84
|
+
"source": "vibe",
|
|
85
|
+
"detail": "score=-0.241"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"timestamp": "2026-10-09T13:15:00.000+00:00",
|
|
89
|
+
"trait": "formality",
|
|
90
|
+
"direction": -1.0,
|
|
91
|
+
"source": "steer",
|
|
92
|
+
"detail": "\"you're too formal\" 0.500→0.390"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"timestamp": "2026-10-09T14:15:00.000+00:00",
|
|
96
|
+
"trait": "riskTolerance",
|
|
97
|
+
"direction": 1.0,
|
|
98
|
+
"source": "steer",
|
|
99
|
+
"detail": "'be bolder' 0.488→0.610"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "imprint-layer"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "A personality calibration layer for any chat model: learns how one user wants an assistant to talk, from their own messages."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["numpy>=1.24"]
|
|
14
|
+
keywords = ["llm", "personalization", "system-prompt", "assistant", "embeddings"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
minilm = ["sentence-transformers>=2.2"]
|
|
22
|
+
test = ["pytest>=7"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
imprint-dashboard = "imprint.dashboard:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
pythonpath = ["src", "tests"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Imprint: a personality calibration layer for any chat model.
|
|
2
|
+
|
|
3
|
+
Learns how one user wants an assistant to talk (8 trait dials plus standing
|
|
4
|
+
rules) from their own messages, and renders a short plain-text block for the
|
|
5
|
+
host to inject into the system prompt.
|
|
6
|
+
"""
|
|
7
|
+
from .core import Imprint, LearnResult, new_profile
|
|
8
|
+
from .embedders import Embedder, MiniLMEmbedder
|
|
9
|
+
from .steering import Steer, detect_steering
|
|
10
|
+
from .traits import TRAIT_NAMES, band_step, describe
|
|
11
|
+
|
|
12
|
+
__all__ = ["Imprint", "LearnResult", "new_profile", "Embedder", "MiniLMEmbedder",
|
|
13
|
+
"Steer", "detect_steering", "TRAIT_NAMES", "band_step", "describe"]
|
|
14
|
+
__version__ = "0.3.0"
|