Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ and versions follow [Semantic Versioning](https://semver.org/).
- A task that repeats the same failing step three times now pauses as stuck and asks what to do, instead of failing.

### Fixed
- **Memories in Hindi stay with the right person:** a Hindi memory is no longer merged into, or skipped as a copy
of, a memory about someone else ("प्रिया पुणे में रहती है" next to "Riya lives in Pune", a Karan memory next to a
Kabir one). Names written in Devanagari are recognized and matched across scripts, so "माया" is still you when
your name is Maya. For people who write in Hindi or another language besides English, setup now suggests the
bge-m3 memory search model (1.2 GB), which finds those memories much better. Changing the memory search model
re-reads every memory once, also between two models of the same size.
- **Sentient can now read a long result to its end:** when a web page or another result is too long to read at
once, Sentient reads the rest of the saved copy part by part instead of seeing only its start again. Reading a
file you downloaded, or that saved copy, now also counts as reading outside content, so Sentient asks before
Expand Down
22 changes: 22 additions & 0 deletions desktop/src/features/onboarding/BrainStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ function LocalBrain() {
<ModelPicker embedding value={d.embedding} onChange={(embedding) => d.set({ embedding })} className="flex-1" />
<ModelTest embedding model={d.embedding} className="shrink-0" />
</div>
<MultilingualHint installed={embedModels.map((m) => m.name)} onShowDownloads={() => setShowPull(true)} />
</Field>
</div>
{(showPull || !embedModels.length) && (
Expand All @@ -167,6 +168,27 @@ function LocalBrain() {
)
}

/** "Writing in Hindi?" (#339): the default memory search model is English-first, so point to a multilingual one. */
function MultilingualHint({ installed, onShowDownloads }: { installed: string[]; onShowDownloads: () => void }) {
const d = useOnboardingDraft()
const hardware = useHardware()
const rec = hardware.data?.recommendation.multilingual_embedding
if (!rec || d.embedding === rec.model) return null
const have = isInstalled(rec.name, installed)
return (
<p className="mt-2 text-xs text-fg-subtle">
Writing in Hindi or another language besides English? {rec.name} ({rec.size}) keeps those memories apart and finds them better.{' '}
<button
type="button"
className="font-medium text-accent hover:underline"
onClick={() => (have ? d.set({ embedding: rec.model }) : onShowDownloads())}
>
{have ? `Use ${rec.name}` : `Download ${rec.name}`}
</button>
</p>
)
}

function isInstalled(name: string, installed: string[]): boolean {
return installed.some((n) => n === name || n === `${name}:latest`)
}
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const ROLE_META: Record<RoleName, RoleMeta> = {
embedding: {
role: 'embedding',
label: 'Embedding',
description: 'Turns memories into vectors so Sentient can find relevant ones. Changing it re-indexes memory.',
description:
'Turns memories into vectors so Sentient can find relevant ones. Changing it re-reads every memory once. For memories in Hindi or other languages, bge-m3 works better.',
required: true,
embedding: true
},
Expand Down Expand Up @@ -220,5 +221,6 @@ export function recommendFastLocal(local: LocalModels | undefined): string | nul
export const SUGGESTED_PULLS = [
{ name: 'qwen3:8b', size: '5.2 GB', note: 'Best all-rounder with tool use' },
{ name: 'qwen3:4b', size: '2.5 GB', note: 'Lighter, good for the fast role' },
{ name: 'nomic-embed-text', size: '274 MB', note: 'Embeddings for memory' }
{ name: 'nomic-embed-text', size: '274 MB', note: 'Embeddings for memory' },
{ name: 'bge-m3', size: '1.2 GB', note: 'Memory search in Hindi and other languages' }
]
2 changes: 2 additions & 0 deletions desktop/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,8 @@ export interface HardwareRecommendation {
/** "qwen3:8b, reading 8,192 tokens at a time" */
summary: string
note: string
/** Memory search model for Hindi and other languages besides English (#339); null when a cloud model comes first. */
multilingual_embedding?: { model: string; name: string; size: string; note: string } | null
}

export interface Hardware {
Expand Down
14 changes: 11 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,9 @@ accept or loosen a rule.
"recommendation": {"tier": "gpu_8", "model": "ollama_chat/qwen3:8b", "name": "qwen3:8b", "context_length": 8192,
"runs_on": "graphics|processor|unknown", "cloud_first": false,
"summary": "qwen3:8b, reading 8,192 tokens at a time",
"note": "Fits on the graphics card, so replies stay quick."}}
"note": "Fits on the graphics card, so replies stay quick.",
"multilingual_embedding": {"model": "ollama/bge-m3", "name": "bge-m3", "size": "1.2 GB",
"note": "If you write to Sentient in Hindi or another language..."}}}
```
Memory comes from the operating system (psutil when installed, otherwise Windows, macOS or Linux APIs). NVIDIA cards
from `nvidia-smi --query-gpu=name,memory.total`, other cards from the Windows registry or `/sys/class/drm` (AMD on
Expand All @@ -436,7 +438,11 @@ accept or loosen a rule.
that `cloud_first: true`: a cloud model is the recommendation and `model` is qwen3:4b only as a labelled chat-only
fallback, never picked for the user, never used by "Local only" or the check-up's fixes); `tier: "unknown"` with today's defaults
when nothing could be read. Onboarding shows it as "Recommended for this computer" and saves its context length with a
local brain; the "Local only" preset and the check-up use it too.
local brain; the "Local only" preset and the check-up use it too. `multilingual_embedding` (#339) is the memory search
model for people who write in Hindi or another language besides English (`MULTILINGUAL_EMBEDDING` in
`config/schema.py`; null when `cloud_first`): onboarding offers it under "Memory search". Switching the `embedding`
role re-embeds memory on its next use, also between two models with the same vector size (the model is recorded with
each vector index).
- `GET /api/models/local` → `{ollama: {reachable, models: [{name, size, family, parameter_size, is_embedding, capabilities: string[]}]}, lm_studio: {reachable, models: [...]}}` (`capabilities` from Ollama, e.g. completion/tools/thinking/vision/embedding — a hint; `POST /api/models/test` is the authoritative tool-support check)
- `POST /api/models/test` `{model, role?}` → `{ok, latency_ms, reply?, error?, supports_tools?}`
- `POST /api/models/test-embedding` `{model}` → `{ok, dim?, error?}`
Expand Down Expand Up @@ -1343,7 +1349,9 @@ Relationships & Social Life, Financial, Goals & Challenges, Miscellaneous.
- `GET /api/memories?topic=&q=&source=&limit=&offset=` → `[Memory]` newest first, expired short-term facts and memories waiting for review excluded (`q` = hybrid search when embeddings are available: vector neighbours plus FTS5 keyword matches, ordered by `score` = `similarity` + `memory.keyword_weight` × share of query words present, each result carrying `similarity` and `score`; falls back to a keyword match)
- `GET /api/memories/topics` → `[{name, description, count}]`
- `GET /api/memories/graph` → `{nodes: [{id, label, title, content, topics, memory_type, source, created_at}], links: [{source, target, value}]}` (`label` = content truncated to 25 chars, `title` = full content, as in v2; a link means cosine similarity ≥ `memory.graph_link_similarity`, `value` is that similarity)
- `POST /api/memories` `{content, source?}` → `{action: "ADD"|"UPDATE"|"DELETE"|"SKIP", id, content, status?: "pending"}` (runs the CUD decision, so a duplicate returns `SKIP` with the existing id; `source` defaults to `manual`. The decision also sees up to 3 facts about the same person and attribute found by keyword (where they live, job, relationship, diet, health, ownership, routine), and a new current residence ("moved to Bengaluru") always UPDATEs the old one ("lives in Pune") rather than adding a second home; past-tense facts are left alone. An UPDATE only replaces a fact about the same person and the same action on the same thing ("doesn't want files written" or "doesn't want emails deleted" never replaces "doesn't want files deleted"; it is added instead), and a new fact that an existing one already says in full, with no new name, place or number ("sister lives in a city" next to "sister Meera lives in Lisbon"), returns `SKIP` with that fact)
- `POST /api/memories` `{content, source?}` → `{action: "ADD"|"UPDATE"|"DELETE"|"SKIP", id, content, status?: "pending"}` (runs the CUD decision, so a duplicate returns `SKIP` with the existing id; `source` defaults to `manual`. The decision also sees up to 3 facts about the same person and attribute found by keyword (where they live, job, relationship, diet, health, ownership, routine), and a new current residence ("moved to Bengaluru") always UPDATEs the old one ("lives in Pune") rather than adding a second home; past-tense facts are left alone. Facts that name someone else are never offered to the model, so it can't update, skip or delete them, and the
near-duplicate shortcut needs the same person too; names in Devanagari (Hindi) count, and match across scripts
("माया" is "Maya", "प्रिया" is not "Riya") (#339). An UPDATE only replaces a fact about the same person and the same action on the same thing ("doesn't want files written" or "doesn't want emails deleted" never replaces "doesn't want files deleted"; it is added instead), and a new fact that an existing one already says in full, with no new name, place or number ("sister lives in a city" next to "sister Meera lives in Lisbon"), returns `SKIP` with that fact)
- `PUT /api/memories/{id}` `{content}` → `Memory` (id kept; topics, long/short-term and expiry re-analyzed; embedding refreshed; 404 if missing)
- `DELETE /api/memories/{id}` → `{deleted: true}` (404 if missing)
- `DELETE /api/memories/source/{source}` → `{deleted: n}`
Expand Down
12 changes: 12 additions & 0 deletions sentient/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ class ModelRoles(BaseModel):
)


# The embedding model recommended for memories in Hindi and other languages besides English (#339). nomic-embed-text
# (the default, 768 dimensions) is English-first: Hindi facts about different people score 0.71 to 0.85 against each
# other, and a Hindi question ranks the wrong person first. bge-m3 (1024 dimensions, 100+ languages, no query prefix)
# scores those pairs 0.35 to 0.56 and the same fact across Hindi and English 0.77 to 0.84. Measured on an RTX 4060
# laptop with Ollama 0.9 (2026-10-14): 1.7 GB loaded on the graphics card (nomic-embed-text 0.85 GB), a memory search
# in 0.6 to 0.7 s once loaded (nomic-embed-text 0.5 to 0.7 s), and switching re-embedded 7 memories in 8 s including
# loading the model. Next to qwen3:8b at 8,192 tokens, Ollama swaps it in when needed: qwen3:8b stays at 7%/93%
# processor/graphics and about 29 tokens/s, and an embedding right after a chat reply takes 5.4 s to load against
# 2.3 s for nomic-embed-text. That cost is why it is offered, not the default.
MULTILINGUAL_EMBEDDING: dict[str, str] = {"model": "ollama/bge-m3", "name": "bge-m3", "size": "1.2 GB"}


class ModelPreset(BaseModel):
"""A saved model setup the user can switch to in one step. Fields left out keep their current value."""

Expand Down
15 changes: 14 additions & 1 deletion sentient/llm/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@

import httpx

from sentient.config.schema import LOCAL_MODEL_TIERS, ModelRoles, ModelsConfig
from sentient.config.schema import (
LOCAL_MODEL_TIERS,
MULTILINGUAL_EMBEDDING,
ModelRoles,
ModelsConfig,
)

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -259,6 +264,12 @@ def summary(hw: dict[str, Any]) -> str:
}


MULTILINGUAL_NOTE = (
"If you write to Sentient in Hindi or another language besides English, this memory search model keeps those "
"memories apart and finds them better. Switching re-reads every memory once."
)


def recommend(hw: dict[str, Any] | None) -> dict[str, Any]:
"""The local model and context length for this computer: the first ``LOCAL_MODEL_TIERS`` row it meets."""
hw = hw or {}
Expand Down Expand Up @@ -289,6 +300,8 @@ def recommend(hw: dict[str, Any] | None) -> dict[str, Any]:
"cloud_first": cloud_first,
"summary": f"a cloud model ({summary} for chat only)" if cloud_first else summary,
"note": NOTES[note_key],
# memory search in Hindi and other languages (#339); it fits wherever the local model does
"multilingual_embedding": None if cloud_first else {**MULTILINGUAL_EMBEDDING, "note": MULTILINGUAL_NOTE},
}


Expand Down
2 changes: 1 addition & 1 deletion sentient/memory/episodic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(self, store: Store, llm: LLMProvider, config: SentientConfig):
async def _embed(self, texts: list[str]) -> list[list[float]]:
vecs = await self.llm.embed(texts)
if vecs:
await self.vec.ensure(len(vecs[0]), self.reindex)
await self.vec.ensure(len(vecs[0]), self.reindex, self.llm.model_for("embedding"))
return vecs

async def reindex(self) -> int:
Expand Down
Loading
Loading