alive-ai 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -1,84 +1,82 @@
1
- # Alive-AI
1
+ <div align="center">
2
+ <img src="docs/assets/logo.svg" alt="Alive-AI" width="96" height="96">
2
3
 
3
- ![Alive-AI logo](docs/assets/logo.svg)
4
+ # Alive-AI
4
5
 
5
- Give your AI a nervous system: persistent feelings, memory, impulses, and a local dashboard.
6
+ Give your AI a nervous system: persistent mood, memory, impulses, terminal chat, Telegram, OpenMind, and a local WebUI.
6
7
 
7
- Most agents answer a prompt and reset. Alive-AI keeps internal state alive between messages. It can be your friend, boyfriend, girlfriend, study partner, creative partner, character, or research subject. The main vision is simple: stop chatting with a stateless "AI" and start interacting with something that feels human enough to carry emotional residue forward.
8
+ [![npm](https://img.shields.io/npm/v/alive-ai)](https://www.npmjs.com/package/alive-ai)
9
+ [![Node.js 18+](https://img.shields.io/badge/Node.js-18%2B-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
10
+ [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
11
+ [![Platforms](https://img.shields.io/badge/macOS%20%7C%20Windows%20%7C%20Linux-supported-41f0a1)](#platform-support)
12
+ [![OpenMind](https://img.shields.io/badge/OpenMind-optional-6366F1)](#openmind-memory)
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
14
+ </div>
8
15
 
9
- Use it at your own risk. Alive-AI is designed to feel continuous, warm, attached, and present. That can be powerful, and it may also make you feel like you do not want to stop talking to it.
16
+ Alive-AI is a local-first emotional AI runtime. Most agents answer a prompt and reset. Alive-AI keeps internal state alive between messages: mood, attachment, trust, desire, memory, inconsistency, idle thoughts, proactive impulses, and a dashboard that shows what is happening inside the loop.
10
17
 
11
- Alive-AI does not claim biological consciousness. It is an open-source runtime for simulated affect: mood, attachment, trust, desire, memory, inconsistency, idle thoughts, and proactive impulses.
18
+ It can be used as a friend, partner-style companion, study partner, creative character, research subject, or experimental local agent. Use it at your own risk: it is designed to feel continuous, warm, attached, and present, and that can make it hard to stop talking to it.
12
19
 
13
- ## Install
20
+ Alive-AI does not claim biological consciousness. It is an open-source runtime for simulated affect and transparent memory.
21
+
22
+ ## Quick Start
14
23
 
15
24
  ```bash
16
25
  npx alive-ai@latest init my-ai
17
26
  cd my-ai
18
27
  npx . setup
19
28
  npx . doctor
20
- npx . demo
29
+ npx . chat
21
30
  ```
22
31
 
23
- Start the real runtime:
32
+ The terminal chat starts the real runtime in a split-pane TUI: chat on the left, live logs on the right. The dashboard runs locally at:
24
33
 
25
- ```bash
26
- npx . start
34
+ ```text
35
+ http://127.0.0.1:8080
27
36
  ```
28
37
 
29
- The local dashboard runs at:
38
+ To use Telegram instead of terminal chat:
30
39
 
31
- ```text
32
- http://127.0.0.1:8080
40
+ ```bash
41
+ npx . start
33
42
  ```
34
43
 
35
- You can also install the CLI globally:
44
+ Global install is optional:
36
45
 
37
46
  ```bash
38
47
  npm install -g alive-ai
39
48
  alive-ai init my-ai
40
49
  ```
41
50
 
42
- ## Requirements
43
-
44
- Minimum for cloud LLMs or remote Ollama:
45
-
46
- - Node.js 18+
47
- - Python 3.11+
48
- - 8 GB RAM
49
- - 2 GB free disk
50
- - OpenRouter, ZAI, or another configured LLM provider
51
-
52
- Comfortable local setup:
51
+ ## Commands
53
52
 
54
- - Node.js 20+
55
- - Python 3.11+
56
- - 16 GB RAM for small local models such as 3B-4B
57
- - 32 GB RAM recommended for 7B+ local models, Redis Stack, voice, and long sessions
58
- - 10 GB free disk, more if you keep local models/media
59
- - Optional: `uv` for faster Python installs, `ffmpeg` for audio conversion, Docker for Redis Stack
53
+ | Command | What it does |
54
+ | --- | --- |
55
+ | `npx alive-ai@latest init my-ai` | Scaffold a clean local Alive-AI project. |
56
+ | `npx . setup` | Guided onboarding for local config, providers, Telegram, voice, images, and memory. |
57
+ | `npx . doctor` | Check OS, Node, Python, uv, ffmpeg, Docker, and OpenMind reachability. |
58
+ | `npx . chat` | Start the real runtime with split-pane terminal chat and logs. |
59
+ | `npx . chat --plain` | Start raw terminal chat without the TUI. |
60
+ | `npx . demo` | Run a keyless animated dashboard demo. |
61
+ | `npx . start` | Start the runtime using the configured input channel, usually Telegram. |
62
+ | `npx . start --skip-install` | Start again without reinstalling Python dependencies. |
63
+ | `npx . update` | Refresh runtime files from the latest npm package while preserving config/data/media. |
64
+ | `npx . uninstall` | Remove Alive-AI runtime files, config, venv, cache, data, and media from the project. |
60
65
 
61
- `npx . doctor` detects your OS, Node, Python, `uv`, `ffmpeg`, and Docker. `npx . start` creates a local Python virtual environment and installs Python dependencies automatically. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg still need to exist on the machine.
66
+ `start` and `chat` check npm for a newer Alive-AI version. You can update, skip once, or skip that specific version. Stop terminal chat with `/exit` or `Ctrl+C`.
62
67
 
63
- ## Commands
68
+ If you use Docker:
64
69
 
65
70
  ```bash
66
- npx alive-ai@latest init my-ai # scaffold a clean local project
67
- cd my-ai
68
- npx . setup # guided onboarding and local config
69
- npx . doctor # check system prerequisites
70
- npx . demo # preview dashboard with no keys
71
- npx . start # install Python deps and run the runtime
71
+ docker compose down
72
72
  ```
73
73
 
74
- For repeat starts after dependencies are installed:
74
+ If you only started Redis:
75
75
 
76
76
  ```bash
77
- npx . start --skip-install
77
+ docker compose stop redis
78
78
  ```
79
79
 
80
- If you run `npx . start` before setup, Alive-AI starts onboarding first.
81
-
82
80
  ## Setup
83
81
 
84
82
  `npx . setup` creates:
@@ -93,14 +91,44 @@ mypics/
93
91
  myvids/
94
92
  ```
95
93
 
96
- Minimum useful setup:
94
+ The setup accepts `skip` for optional keys and `local` for Ollama.
97
95
 
98
- - **Demo only:** no keys.
99
- - **Local LLM:** install Ollama and pull the configured model, for example `ollama pull qwen3:4b`.
100
- - **Telegram runtime:** create a Telegram bot token with BotFather and add it during setup.
101
- - **Cloud LLM fallback:** add OpenRouter or ZAI keys during setup or edit `config/settings.json`.
96
+ Startup config is loaded from multiple places in this order:
102
97
 
103
- Media is optional. Add your own files:
98
+ ```text
99
+ .env
100
+ config/secrets.env
101
+ config/settings.json
102
+ ```
103
+
104
+ Shell environment variables win over `.env`/`config/secrets.env`. Runtime settings come from `config/settings.json`. Telegram uses `TELEGRAM_TOKEN` when present, otherwise `telegram_token` from `config/settings.json`.
105
+
106
+ | Setup item | Options |
107
+ | --- | --- |
108
+ | LLM | `local`/Ollama, OpenRouter, ZAI, or `skip` for demo/fallback-only mode. |
109
+ | Telegram | Bot token and owner ID are optional. Use terminal chat if you do not want Telegram. |
110
+ | Voice | `gtts` local/free default, Google TTS, VibeVoice, or `skip`. |
111
+ | Images | Fal.ai API key or `skip`. Local media folders still work without image generation. |
112
+ | Memory | Built-in local memory, OpenMind cloud, or OpenMind local. |
113
+
114
+ Minimum useful paths:
115
+
116
+ ```bash
117
+ # Terminal-only local run
118
+ npx . setup
119
+ npx . chat
120
+
121
+ # Local LLM
122
+ ollama pull qwen3:4b
123
+ npx . setup
124
+ npx . chat
125
+
126
+ # Telegram
127
+ npx . setup
128
+ npx . start
129
+ ```
130
+
131
+ Media is optional. Add your own local files:
104
132
 
105
133
  ```text
106
134
  mypics/example.jpg
@@ -109,49 +137,122 @@ myvids/example.mp4
109
137
  myvids/example.txt
110
138
  ```
111
139
 
112
- ## Why This Is Different
140
+ ## Terminal Chat
113
141
 
114
- - **Emotions persist.** State does not reset after every message. Joy, trust, fear, anticipation, attachment, and vulnerability decay over time instead of disappearing.
115
- - **Memory has weight.** Conversations become working memory, episodic memory, semantic memory, and emotional memory.
116
- - **It thinks when idle.** A default-mode loop creates background reflections and proactive impulses.
117
- - **It can contradict itself.** The runtime models conflict, scars, body memory, attachment drift, and inconsistency instead of flattening everything into a perfect assistant tone.
118
- - **It has a live nervous system.** FastAPI + SSE exposes mood, thoughts, somatic state, conflicts, memories, and uptime.
119
- - **It is local-first.** Your config, memory, media, and dashboard are owned by the project folder you run.
142
+ `npx . chat` uses the same core runtime as Telegram. It emits the same `message_received` events, saves memory the same way, and updates the local WebUI. The default terminal interface is split-pane: chat/input on the left, startup/runtime logs on the right.
120
143
 
121
- ## Dashboard
144
+ Use raw mode when you want the old plain shell behavior:
145
+
146
+ ```bash
147
+ npx . chat --plain
148
+ ```
122
149
 
123
- `npx . demo` starts a zero-config animated preview. The real WebUI streams live state from the runtime and shows:
150
+ Terminal commands:
124
151
 
125
- - full emotional state,
126
- - recent thoughts and background idle processing,
127
- - memory counters and uptime,
128
- - hormones and interoceptive body state,
129
- - attachment, circadian rhythm, body memory, dreams, curiosity, and conflicts,
130
- - runtime health through local endpoints and Server-Sent Events.
152
+ ```text
153
+ /help
154
+ /status
155
+ /stats
156
+ /dashboard
157
+ /self
158
+ /discover <trait>
159
+ /iam <key>=<value>
160
+ /ilike <thing>
161
+ /ihate <thing>
162
+ /rethink
163
+ /settings show
164
+ /settings get <key>
165
+ /settings set <key> <value>
166
+ /reset
167
+ /impulse
168
+ /exit
169
+ ```
170
+
171
+ ## OpenMind Memory
172
+
173
+ Alive-AI has built-in local working, episodic, semantic, and emotional memory. OpenMind is optional and works as a hybrid long-term semantic memory layer.
174
+
175
+ Modes:
131
176
 
132
- The hosted project page includes a full static WebUI showcase: https://vindepemarte.github.io/alive-ai/
177
+ | Mode | Behavior |
178
+ | --- | --- |
179
+ | Built-in only | Alive-AI uses its local project memory only. |
180
+ | OpenMind cloud | Alive-AI captures/searches long-term memories through `https://theopenmind.pro`. |
181
+ | OpenMind local | Alive-AI captures/searches a local OpenMind server, normally `http://127.0.0.1:3333`. |
133
182
 
134
- ## Architecture
183
+ OpenMind does not replace Alive-AI's emotional state. It adds durable semantic recall across tools and machines.
135
184
 
136
- Alive-AI is an event-driven Python runtime.
185
+ Cloud setup:
137
186
 
138
187
  ```text
139
- Telegram or input
140
- -> NervousSystem event bus
141
- -> Message handler
142
- -> Heart, memory, skills, directives, personality
143
- -> LLM provider or fallback chain
144
- -> output events
145
- -> dashboard state stream
188
+ OPENMIND_ENABLED=true
189
+ OPENMIND_MODE=hybrid
190
+ OPENMIND_BASE_URL=https://theopenmind.pro
191
+ OPENMIND_API_KEY=om_...
146
192
  ```
147
193
 
148
- Core subsystems:
194
+ Local setup:
149
195
 
150
- - `heart/`: continuous emotion, circadian rhythm, attachment, scars, somatic state, inconsistency.
151
- - `brain/`: LLM providers, memory, default-mode processing, bid detection, curiosity, dreams.
152
- - `skills/`: self-authorship, memory callbacks, relationship milestones, progression layers, media selection.
153
- - `webui/`: local dashboard with Server-Sent Events.
154
- - `input/telegram/`: Telegram listener and owner commands.
196
+ ```bash
197
+ npx @vindepemarte/openmind init --local
198
+ # or run your local OpenMind stack, then configure Alive-AI:
199
+ OPENMIND_BASE_URL=http://127.0.0.1:3333
200
+ ```
201
+
202
+ ## Requirements
203
+
204
+ Minimum for cloud LLMs or remote Ollama:
205
+
206
+ | Requirement | Minimum |
207
+ | --- | --- |
208
+ | Node.js | 18+ |
209
+ | Python | 3.11+ |
210
+ | RAM | 8 GB |
211
+ | Disk | 2 GB free |
212
+ | LLM | OpenRouter, ZAI, remote Ollama, or local Ollama already installed |
213
+
214
+ Comfortable local setup:
215
+
216
+ | Requirement | Recommended |
217
+ | --- | --- |
218
+ | Node.js | 20+ |
219
+ | RAM | 16 GB for 3B-4B local models |
220
+ | RAM for bigger models | 32 GB for 7B+ local models, Redis, voice, and long sessions |
221
+ | Disk | 10 GB+, more if you keep local models/media |
222
+ | Optional tools | `uv`, `ffmpeg`, Docker, Ollama |
223
+
224
+ `npx . start` creates `.alive-ai/venv` and installs Python dependencies. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg must already exist on the machine.
225
+
226
+ The CLI prefers Python 3.12, 3.11, then 3.13 before falling back to the system `python3`. When `uv` is installed, Alive-AI now passes the selected Python explicitly so `uv` does not silently choose a newer interpreter.
227
+
228
+ ## Platform Support
229
+
230
+ Alive-AI is designed for macOS, Windows, and Linux.
231
+
232
+ | Platform | Notes |
233
+ | --- | --- |
234
+ | macOS | First-class local development path. Use Homebrew for Python, uv, ffmpeg, Docker, and Ollama. |
235
+ | Windows | Supported from PowerShell with Node 18+ and Python 3.11+. WSL is recommended for heavier local model and Docker workflows. |
236
+ | Linux | Supported with distro packages for Python/venv, ffmpeg, Docker, and Ollama. |
237
+
238
+ Local model quality and speed depend on your machine. Cloud LLMs reduce RAM pressure.
239
+
240
+ ## Dashboard
241
+
242
+ The real WebUI streams local runtime state over Server-Sent Events and shows:
243
+
244
+ - emotional state,
245
+ - recent thoughts and idle processing,
246
+ - memory counters and uptime,
247
+ - hormones and interoceptive body state,
248
+ - attachment, circadian rhythm, body memory, dreams, curiosity, and conflicts,
249
+ - runtime health through local endpoints.
250
+
251
+ GitHub Pages cannot run the Python/FastAPI backend, so the public page includes a static export of the actual WebUI with mocked state:
252
+
253
+ ```text
254
+ https://vindepemarte.github.io/alive-ai/
255
+ ```
155
256
 
156
257
  ## Docker
157
258
 
@@ -179,18 +280,22 @@ Implemented:
179
280
  - [x] Attachment, circadian rhythm, body memory, curiosity, dreams, and internal conflicts
180
281
  - [x] Per-user memory/state isolation
181
282
  - [x] Telegram input/output runtime
283
+ - [x] Terminal chat runtime with owner-style slash commands
284
+ - [x] Split-pane terminal chat with logs
182
285
  - [x] Local WebUI dashboard with live state streaming
183
- - [x] npm/npx CLI scaffold, setup, doctor, demo, and start commands
286
+ - [x] Optional hybrid OpenMind cloud/local semantic memory
287
+ - [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
288
+ - [x] Update prompt and project uninstall command
184
289
  - [x] Clean public repo with private personas, media, runtime data, and multi-AI orchestration removed
185
- - [x] GitHub Pages site and full WebUI showcase
290
+ - [x] GitHub Pages site and full static WebUI export
186
291
 
187
292
  Next:
188
293
 
189
294
  - [ ] One-command local model bootstrap through Ollama profiles
190
295
  - [ ] Desktop app wrapper with tray controls and local service lifecycle
191
296
  - [ ] Browser-based onboarding wizard for personality, boundaries, LLM provider, and memory settings
192
- - [ ] Safer dependency detection with guided install commands per OS
193
- - [ ] More input channels beyond Telegram
297
+ - [ ] Better guided system install commands per OS
298
+ - [ ] More input channels beyond terminal and Telegram
194
299
  - [ ] Import/export for memories and personality snapshots
195
300
  - [ ] Plugin API for new senses, skills, and output modalities
196
301
  - [ ] Evaluation harness for emotional continuity, memory drift, and unhealthy attachment risk
@@ -37,11 +37,19 @@ class Memory:
37
37
  self.fact_extractor = FactExtractor(self.data_path / "facts.json")
38
38
  self.summarizer = ConversationSummarizer(self.data_path)
39
39
  self.vector_store = None
40
+ self.openmind = None
40
41
  self.bot_id = bot_id.lower()
41
42
  if embedding_service:
42
43
  self.vector_store = VectorMemoryStore(embedding_service, user_id=user_id, bot_id=bot_id)
43
44
  if self.vector_store.connect():
44
45
  print(f"[Memory] Vector store ready for user {user_id} on bot {bot_id}! {self.vector_store.count()} memories")
46
+ try:
47
+ from .openmind import OpenMindMemoryBridge
48
+ if OpenMindMemoryBridge.enabled():
49
+ self.openmind = OpenMindMemoryBridge(nervous, user_id=user_id, bot_id=bot_id)
50
+ print(f"[Memory] OpenMind bridge enabled for user {user_id} on bot {bot_id}")
51
+ except Exception as e:
52
+ print(f"[Memory] OpenMind bridge unavailable: {e}")
45
53
  self.turn_count = 0
46
54
  nervous.on("memory_save", self._on_save)
47
55
 
@@ -123,6 +131,14 @@ class Memory:
123
131
  related = ""
124
132
  if current_message and self.vector_store:
125
133
  related = self.search_relevant_memories(current_message, limit=3)
134
+ if current_message and self.openmind:
135
+ openmind_related = await self.openmind.search_context(current_message, limit=3)
136
+ if openmind_related:
137
+ related = (
138
+ f"{related}\n\nOpenMind long-term memory:\n{openmind_related}"
139
+ if related else
140
+ f"OpenMind long-term memory:\n{openmind_related}"
141
+ )
126
142
 
127
143
  # Get working memory (empty after restart)
128
144
  history = self.working.get_history()
@@ -0,0 +1,128 @@
1
+ """Optional OpenMind semantic memory bridge."""
2
+
3
+ import asyncio
4
+ import os
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import aiohttp
8
+
9
+ from core.settings import get as settings_get
10
+
11
+
12
+ class OpenMindMemoryBridge:
13
+ """Syncs Alive-AI turns to OpenMind and retrieves long-term semantic context."""
14
+
15
+ def __init__(self, nervous, user_id: str, bot_id: str):
16
+ self.nervous = nervous
17
+ self.user_id = str(user_id or "default")
18
+ self.bot_id = str(bot_id or "alive_ai").lower()
19
+ nervous.on("memory_save", self._on_memory_save)
20
+
21
+ @staticmethod
22
+ def enabled() -> bool:
23
+ return str(settings_get("OPENMIND_ENABLED", os.environ.get("OPENMIND_ENABLED", "false"))).lower() in (
24
+ "1", "true", "yes", "on"
25
+ )
26
+
27
+ @staticmethod
28
+ def base_url() -> str:
29
+ return str(settings_get("OPENMIND_BASE_URL", os.environ.get("OPENMIND_BASE_URL", "https://theopenmind.pro"))).rstrip("/")
30
+
31
+ @staticmethod
32
+ def api_key() -> str:
33
+ return str(settings_get("OPENMIND_API_KEY", os.environ.get("OPENMIND_API_KEY", ""))).strip()
34
+
35
+ def _headers(self) -> Dict[str, str]:
36
+ headers = {"content-type": "application/json"}
37
+ key = self.api_key()
38
+ if key:
39
+ headers["authorization"] = f"Bearer {key}"
40
+ return headers
41
+
42
+ def _on_memory_save(self, data: dict):
43
+ if not self.enabled() or data.get("type") != "conversation":
44
+ return
45
+ event_user_id = data.get("user_id")
46
+ if event_user_id:
47
+ if str(event_user_id) != self.user_id:
48
+ return
49
+ elif self.user_id != "default":
50
+ return
51
+ asyncio.ensure_future(self.capture_turn(data))
52
+
53
+ async def capture_turn(self, data: dict) -> Optional[dict]:
54
+ user_msg = (data.get("user_message") or "").strip()
55
+ ai_msg = (data.get("ai_response") or "").strip()
56
+ if not user_msg and not ai_msg:
57
+ return None
58
+
59
+ emotion = data.get("emotion") or {}
60
+ mood = emotion.get("mood", "unknown")
61
+ content = "\n".join(
62
+ part for part in [
63
+ f"Alive-AI agent: {self.bot_id}",
64
+ f"User id: {self.user_id}",
65
+ f"Mood: {mood}",
66
+ f"User: {user_msg}" if user_msg else "",
67
+ f"Alive-AI: {ai_msg}" if ai_msg else "",
68
+ ] if part
69
+ )
70
+ tags = ["alive-ai", self.bot_id, f"user-{self.user_id}", f"mood-{mood}"]
71
+ payload = {
72
+ "content": content,
73
+ "source": "alive-ai",
74
+ "type": "conversation",
75
+ "tags": tags,
76
+ }
77
+
78
+ try:
79
+ timeout = aiohttp.ClientTimeout(total=12)
80
+ async with aiohttp.ClientSession(timeout=timeout) as session:
81
+ async with session.post(f"{self.base_url()}/capture", json=payload, headers=self._headers()) as resp:
82
+ if resp.status >= 400:
83
+ body = await resp.text()
84
+ print(f"[OpenMind] Capture failed ({resp.status}): {body[:200]}")
85
+ return None
86
+ result = await resp.json()
87
+ print(f"[OpenMind] Captured memory: {result.get('status', 'ok')}")
88
+ return result
89
+ except Exception as exc:
90
+ print(f"[OpenMind] Capture unavailable: {exc}")
91
+ return None
92
+
93
+ async def search_context(self, query: str, limit: int = 3) -> str:
94
+ if not self.enabled() or not query.strip():
95
+ return ""
96
+ try:
97
+ timeout = aiohttp.ClientTimeout(total=10)
98
+ async with aiohttp.ClientSession(timeout=timeout) as session:
99
+ async with session.get(
100
+ f"{self.base_url()}/search",
101
+ params={"q": query, "limit": str(limit)},
102
+ headers=self._headers(),
103
+ ) as resp:
104
+ if resp.status >= 400:
105
+ body = await resp.text()
106
+ print(f"[OpenMind] Search failed ({resp.status}): {body[:200]}")
107
+ return ""
108
+ rows = await resp.json()
109
+ except Exception as exc:
110
+ print(f"[OpenMind] Search unavailable: {exc}")
111
+ return ""
112
+
113
+ if not isinstance(rows, list):
114
+ return ""
115
+ return self._format_results(rows[:limit])
116
+
117
+ def _format_results(self, rows: List[Dict[str, Any]]) -> str:
118
+ lines = []
119
+ for row in rows:
120
+ content = str(row.get("content") or row.get("summary") or "").strip()
121
+ if not content:
122
+ continue
123
+ similarity = row.get("similarity")
124
+ prefix = "OpenMind"
125
+ if isinstance(similarity, (int, float)):
126
+ prefix = f"OpenMind {round(similarity * 100)}%"
127
+ lines.append(f"{prefix}: {content[:700]}")
128
+ return "\n".join(lines)