Compare commits
10 Commits
873e7535fb
...
0910affc78
| Author | SHA1 | Date | |
|---|---|---|---|
| 0910affc78 | |||
| 9fc6ad20d2 | |||
| 306dcfe167 | |||
| 28977ba295 | |||
| 0bbfb3bf1e | |||
| 79b3df4de2 | |||
| 0a99922f24 | |||
| 8f175d3f8f | |||
| 00f1dfaec5 | |||
| ab23e40b28 |
2
.github/BRANCH_PROTECTION.md
vendored
2
.github/BRANCH_PROTECTION.md
vendored
@ -1,6 +1,6 @@
|
|||||||
# Branch Protection Baseline
|
# Branch Protection Baseline
|
||||||
|
|
||||||
Use this as the admin checklist for `main` after the EverOS 1.0 history reset.
|
Use this as the admin checklist for `main` after the EverOS 1.0.0 history reset.
|
||||||
|
|
||||||
## Required Repository Rule
|
## Required Repository Rule
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -11,7 +11,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Area
|
label: Area
|
||||||
options:
|
options:
|
||||||
- methods/EverCore
|
- src/everos
|
||||||
- methods/HyperMem
|
- methods/HyperMem
|
||||||
- benchmarks/EverMemBench
|
- benchmarks/EverMemBench
|
||||||
- benchmarks/EvoAgentBench
|
- benchmarks/EvoAgentBench
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/docs.yml
vendored
2
.github/ISSUE_TEMPLATE/docs.yml
vendored
@ -7,7 +7,7 @@ body:
|
|||||||
id: page
|
id: page
|
||||||
attributes:
|
attributes:
|
||||||
label: Page or file
|
label: Page or file
|
||||||
placeholder: README.md, methods/EverCore/docs/...
|
placeholder: README.md, docs/architecture.md, use-cases/...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
12
.github/workflows/commits.yml
vendored
12
.github/workflows/commits.yml
vendored
@ -13,6 +13,18 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
title:
|
||||||
|
name: pull request title
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Validate Conventional Commit PR title
|
||||||
|
env:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
run: make check-pr-title
|
||||||
|
|
||||||
messages:
|
messages:
|
||||||
name: commit messages
|
name: commit messages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
35
.gitignore
vendored
35
.gitignore
vendored
@ -165,5 +165,40 @@ outputs/
|
|||||||
logs/
|
logs/
|
||||||
nohup.out
|
nohup.out
|
||||||
|
|
||||||
|
# Repository media policy: do not commit images, videos, or asset/media folders.
|
||||||
|
# Use external hosting, release artifacts, or approved storage and link from docs.
|
||||||
|
asset/
|
||||||
|
assets/
|
||||||
|
image/
|
||||||
|
images/
|
||||||
|
img/
|
||||||
|
media/
|
||||||
|
video/
|
||||||
|
videos/
|
||||||
|
*.avif
|
||||||
|
*.bmp
|
||||||
|
*.gif
|
||||||
|
*.heic
|
||||||
|
*.heif
|
||||||
|
*.icns
|
||||||
|
*.ico
|
||||||
|
*.jpeg
|
||||||
|
*.jpg
|
||||||
|
*.png
|
||||||
|
*.svg
|
||||||
|
*.tif
|
||||||
|
*.tiff
|
||||||
|
*.webp
|
||||||
|
*.avi
|
||||||
|
*.flv
|
||||||
|
*.m4v
|
||||||
|
*.mkv
|
||||||
|
*.mov
|
||||||
|
*.mp4
|
||||||
|
*.mpeg
|
||||||
|
*.mpg
|
||||||
|
*.webm
|
||||||
|
*.wmv
|
||||||
|
|
||||||
# Use-cases: exclude lock files to keep the repo lean
|
# Use-cases: exclude lock files to keep the repo lean
|
||||||
use-cases/**/package-lock.json
|
use-cases/**/package-lock.json
|
||||||
|
|||||||
@ -28,6 +28,19 @@ repos:
|
|||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: no-repo-assets
|
||||||
|
name: block committed images, videos, and asset directories
|
||||||
|
entry: python3 scripts/check_repo_assets.py
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
- id: no-deprecated-product-names
|
||||||
|
name: block deprecated product names
|
||||||
|
entry: python3 scripts/check_deprecated_names.py
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
- repo: https://github.com/jorisroovers/gitlint
|
- repo: https://github.com/jorisroovers/gitlint
|
||||||
rev: v0.19.1
|
rev: v0.19.1
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@ -42,9 +42,11 @@ Use the [feature request template](https://github.com/EverMind-AI/EverOS/issues/
|
|||||||
|
|
||||||
1. Create a branch from `main`.
|
1. Create a branch from `main`.
|
||||||
2. Keep the change scoped to one purpose.
|
2. Keep the change scoped to one purpose.
|
||||||
3. Run `make ci` locally before requesting review.
|
3. Do not commit images, videos, or asset/media directories. Use external
|
||||||
4. Use a Conventional Commit title, such as `fix(search): guard empty profile`.
|
hosting, release artifacts, or approved storage and link from docs.
|
||||||
5. Open a pull request to `main` and fill out the PR template.
|
4. Run `make ci` locally before requesting review.
|
||||||
|
5. Use a Conventional Commit title, such as `fix(search): guard empty profile`.
|
||||||
|
6. Open a pull request to `main` and fill out the PR template.
|
||||||
|
|
||||||
By submitting a pull request, you agree that your contribution is licensed under
|
By submitting a pull request, you agree that your contribution is licensed under
|
||||||
the project's [Apache-2.0](LICENSE) license.
|
the project's [Apache-2.0](LICENSE) license.
|
||||||
@ -90,7 +92,7 @@ git clone https://github.com/EverMind-AI/EverOS.git
|
|||||||
cd EverOS
|
cd EverOS
|
||||||
make install # deps + pre-commit hooks (one-stop dev setup)
|
make install # deps + pre-commit hooks (one-stop dev setup)
|
||||||
everos init # write ./.env, then fill in the API key slots
|
everos init # write ./.env, then fill in the API key slots
|
||||||
make ci # verify: lint + unit + integration
|
make ci # verify: lint + unit + integration + package
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
@ -110,7 +112,7 @@ Highlights:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make format # ruff fix + format
|
make format # ruff fix + format
|
||||||
make lint # ruff check + import-linter
|
make lint # ruff check + import-linter + hard repo hygiene gates
|
||||||
```
|
```
|
||||||
|
|
||||||
### Branch strategy
|
### Branch strategy
|
||||||
|
|||||||
21
Makefile
21
Makefile
@ -1,12 +1,15 @@
|
|||||||
.PHONY: help install install-deps lint docs-check check-commits check-cjk check-datetime openapi check-openapi format test integration package cov ci clean
|
.PHONY: help install install-deps lint docs-check check-commits check-pr-title check-assets check-deprecated-names check-cjk check-datetime openapi check-openapi format test integration package cov ci clean
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " install Install deps + pre-commit hooks (full dev setup)"
|
@echo " install Install deps + pre-commit hooks (full dev setup)"
|
||||||
@echo " install-deps Install deps only (uv sync --frozen, used by CI)"
|
@echo " install-deps Install deps only (uv sync --frozen, used by CI)"
|
||||||
@echo " lint ruff (check + format-check) + import-linter + datetime discipline + openapi drift"
|
@echo " lint ruff + import-linter + repo hygiene + datetime discipline + openapi drift"
|
||||||
@echo " docs-check Validate Markdown links, use-case banners, and issue template YAML"
|
@echo " docs-check Validate Markdown links, use-case banners, and issue template YAML"
|
||||||
@echo " check-commits Validate Conventional Commit subjects for a git range"
|
@echo " check-commits Validate Conventional Commit subjects for a git range"
|
||||||
|
@echo " check-pr-title Validate PR title uses Conventional Commit format"
|
||||||
|
@echo " check-assets Block committed images, videos, and asset/media directories"
|
||||||
|
@echo " check-deprecated-names Block deprecated product names"
|
||||||
@echo " check-cjk Scan for CJK outside the language-policy allowlist (advisory)"
|
@echo " check-cjk Scan for CJK outside the language-policy allowlist (advisory)"
|
||||||
@echo " check-datetime Scan for code that bypasses component/utils/datetime (HARD gate, run via lint)"
|
@echo " check-datetime Scan for code that bypasses component/utils/datetime (HARD gate, run via lint)"
|
||||||
@echo " openapi Regenerate docs/openapi.json from the FastAPI app"
|
@echo " openapi Regenerate docs/openapi.json from the FastAPI app"
|
||||||
@ -34,6 +37,8 @@ lint:
|
|||||||
uv run ruff check src tests
|
uv run ruff check src tests
|
||||||
uv run ruff format --check src tests
|
uv run ruff format --check src tests
|
||||||
uv run lint-imports
|
uv run lint-imports
|
||||||
|
uv run python scripts/check_repo_assets.py
|
||||||
|
uv run python scripts/check_deprecated_names.py
|
||||||
uv run python scripts/check_datetime_discipline.py
|
uv run python scripts/check_datetime_discipline.py
|
||||||
uv run python scripts/dump_openapi.py --check
|
uv run python scripts/dump_openapi.py --check
|
||||||
|
|
||||||
@ -44,6 +49,18 @@ docs-check:
|
|||||||
check-commits:
|
check-commits:
|
||||||
python3 scripts/check_commit_messages.py $(RANGE)
|
python3 scripts/check_commit_messages.py $(RANGE)
|
||||||
|
|
||||||
|
check-pr-title:
|
||||||
|
python3 scripts/check_pr_title.py
|
||||||
|
|
||||||
|
# Repository media hygiene gate. Images/videos belong in external hosting,
|
||||||
|
# release artifacts, or other approved storage, then linked from docs.
|
||||||
|
check-assets:
|
||||||
|
uv run python scripts/check_repo_assets.py
|
||||||
|
|
||||||
|
# Product naming gate. Public repo text should use EverOS or EverMind Cloud.
|
||||||
|
check-deprecated-names:
|
||||||
|
uv run python scripts/check_deprecated_names.py
|
||||||
|
|
||||||
# Advisory CJK scan (see .claude/rules/language-policy.md). Deliberately NOT
|
# Advisory CJK scan (see .claude/rules/language-policy.md). Deliberately NOT
|
||||||
# wired into `lint` / `ci`: the policy is enforced by review and the rules
|
# wired into `lint` / `ci`: the policy is enforced by review and the rules
|
||||||
# doc, not a hard gate. Run on demand when touching potentially-CJK files.
|
# doc, not a hard gate. Run on demand when touching potentially-CJK files.
|
||||||
|
|||||||
364
README.md
364
README.md
@ -1,6 +1,6 @@
|
|||||||
<div align="center" id="readme-top">
|
<div align="center" id="readme-top">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://x.com/evermind"><img src="https://img.shields.io/badge/EverMind-000000?labelColor=gray&style=for-the-badge&logo=x&logoColor=white" alt="X"></a>
|
<a href="https://x.com/evermind"><img src="https://img.shields.io/badge/EverMind-000000?labelColor=gray&style=for-the-badge&logo=x&logoColor=white" alt="X"></a>
|
||||||
@ -9,27 +9,29 @@
|
|||||||
<a href="https://github.com/EverMind-AI/EverOS/discussions/67"><img src="https://img.shields.io/badge/WeCom-EverMind_社区-07C160?labelColor=gray&style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>
|
<a href="https://github.com/EverMind-AI/EverOS/discussions/67"><img src="https://img.shields.io/badge/WeCom-EverMind_社区-07C160?labelColor=gray&style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[Website](https://evermind.ai) · [Documentation](https://docs.evermind.ai) · [Blog](https://evermind.ai/blogs)
|
[Website](https://evermind.ai) · [Documentation](https://docs.evermind.ai) · [Blog](https://evermind.ai/blogs) · [中文](README.zh-CN.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<details open>
|
<details>
|
||||||
<summary><kbd>Table of Contents</kbd></summary>
|
<summary><kbd>Table of Contents</kbd></summary>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
- [What is EverOS](#what-is-everos)
|
- [EverOS 1.0.0 Highlights](#everos-100-highlights)
|
||||||
- [Architecture at a glance](#architecture-at-a-glance)
|
- [Why EverOS](#why-everos)
|
||||||
- [Quick start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
- [Storage layout](#storage-layout)
|
- [Architecture At A Glance](#architecture-at-a-glance)
|
||||||
|
- [Storage Layout](#storage-layout)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Project structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [Use Cases](#use-cases)
|
- [Use Cases](#use-cases)
|
||||||
- [Stay Tuned](#stay-tuned)
|
- [Watch EverOS](#watch-everos)
|
||||||
|
- [EverMind Ecosystems](#evermind-ecosystems)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@ -37,48 +39,120 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## What is EverOS
|
## EverOS 1.0.0 Highlights
|
||||||
|
|
||||||
EverOS is an open-source Python framework that turns conversations, agent trajectories, and files into **structured, retrievable, evolving long-term memory** for AI agents and user chats. Designed for **lightweight local deployments** (small teams, individual developers), with three core principles:
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> **EverOS 1.0.0 is a major release for self-evolving memory.** It brings a
|
||||||
|
> local-first runtime, Markdown as the source of truth, hybrid retrieval,
|
||||||
|
> multimodal ingestion, user and agent memory scopes, and modular algorithms
|
||||||
|
> through [EverAlgo](https://github.com/EverMind-AI/EverAlgo).
|
||||||
|
>
|
||||||
|
> **Watch this repository** for the next wave of memory-system work, including
|
||||||
|
> Wiki-style knowledge layers and Dreaming for deeper offline evolution.
|
||||||
|
|
||||||
1. **Markdown as Source of Truth** — All memory persists as plain `.md` files. Open, edit, grep, version with Git, view in Obsidian. No black-box database lock-in.
|
<table>
|
||||||
2. **Lightweight three-piece storage** — `Markdown` files (truth) + `SQLite` (state/queue) + `LanceDB` (vector + BM25 + scalar). No MongoDB / Elasticsearch / Milvus / Redis / Kafka required.
|
<tr>
|
||||||
3. **[EverAlgo](https://github.com/EverMind-AI/EverAlgo) as pure algorithm library** — Memory extraction algorithms are decoupled into a separate library; this project orchestrates and persists.
|
<td width="33%" valign="top">
|
||||||
|
<strong>Markdown As Source Of Truth</strong><br>
|
||||||
|
<br>
|
||||||
|
All memory is persisted as <code>.md</code> files: readable, editable,
|
||||||
|
grep-able, Git-versioned, and openable directly in Obsidian.
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Local Three-Part Stack</strong><br>
|
||||||
|
<br>
|
||||||
|
Markdown + SQLite + LanceDB keep vectors, BM25, and scalar filters
|
||||||
|
local. No MongoDB, Elasticsearch, or Redis required.
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Dual-Track Memory</strong><br>
|
||||||
|
<br>
|
||||||
|
Agent memory (<code>cases</code> / <code>skills</code>) and user memory
|
||||||
|
(<code>episodes</code> / <code>profile</code>) are extracted independently.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Multimodal Ingestion</strong><br>
|
||||||
|
<br>
|
||||||
|
Text, images, audio, documents, PDFs, HTML, and email are unified into
|
||||||
|
searchable memory.
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Self-Evolution</strong><br>
|
||||||
|
<br>
|
||||||
|
Common skills are extracted from real usage; repeated patterns become
|
||||||
|
reusable workflows, no retraining required.
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Orthogonal Retrieval</strong><br>
|
||||||
|
<br>
|
||||||
|
Search independently by <code>user_id</code>, <code>agent_id</code>,
|
||||||
|
<code>app_id</code>, <code>project_id</code>, and <code>session_id</code>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
## Architecture at a glance
|
[](#readme-top)
|
||||||
|
|
||||||
```
|
</div>
|
||||||
┌───────────────────────────────────────────────┐
|
|
||||||
│ entrypoints/ (CLI + HTTP API) │ presentation
|
|
||||||
├───────────────────────────────────────────────┤
|
|
||||||
│ service/ (use cases: memorize/retrieve) │ application
|
|
||||||
├───────────────────────────────────────────────┤
|
|
||||||
│ memory/ (extract + search + cascade) │ domain
|
|
||||||
├───────────────────────────────────────────────┤
|
|
||||||
│ infra/ (markdown / sqlite / lancedb) │ infrastructure
|
|
||||||
└───────────────────────────────────────────────┘
|
|
||||||
↑ ↑
|
|
||||||
component/ core/
|
|
||||||
(LLM/Embedding) (observability/lifespan)
|
|
||||||
```
|
|
||||||
|
|
||||||
DDD 5 layers, single-direction dependency. See [docs/architecture.md](docs/architecture.md).
|
|
||||||
|
## Why EverOS
|
||||||
|
|
||||||
|
EverOS is an open-source Python framework for self-evolving long-term
|
||||||
|
memory across agents and platforms. It gives makers one portable memory
|
||||||
|
layer for every agent they use - Claude Code, Codex, OpenClaw, Hermes,
|
||||||
|
and more - so context, decisions, files, and trajectories can follow the
|
||||||
|
work instead of staying trapped in one tool.
|
||||||
|
|
||||||
|
EverOS stores conversations, agent trajectories, and files as readable
|
||||||
|
Markdown, then syncs local SQLite and LanceDB indexes for fast retrieval.
|
||||||
|
Agents can reuse past cases and skills, improve from repeated workflows,
|
||||||
|
and become more proactive over time.
|
||||||
|
|
||||||
|
The system is built around three boundaries:
|
||||||
|
|
||||||
|
1. **Memory content stays readable** - Markdown is the durable source of truth.
|
||||||
|
2. **Runtime state stays local** - SQLite tracks state and LanceDB handles vector, BM25, and scalar-filter search.
|
||||||
|
3. **Algorithms stay modular** - [EverAlgo](https://github.com/EverMind-AI/EverAlgo) owns memory algorithms; EverOS owns runtime, persistence, online flows, and offline evolution.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
## Quick start
|
[](#readme-top)
|
||||||
|
|
||||||
### Install as a package
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install EverOS
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv pip install everos # or: pip install everos
|
uv pip install everos
|
||||||
|
# or: pip install everos
|
||||||
|
```
|
||||||
|
|
||||||
# Generate a starter .env (OpenRouter + DeepInfra defaults; bundled inside the wheel)
|
### 2. Initialize Configuration
|
||||||
everos init # writes ./.env (use --xdg for ~/.config/everos/.env)
|
|
||||||
# Edit .env and fill the API key fields (see comments inside).
|
|
||||||
|
|
||||||
|
Generate a starter `.env` file, then fill the API key fields shown in
|
||||||
|
the generated comments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
everos init
|
||||||
|
```
|
||||||
|
|
||||||
|
`everos init` writes `./.env` by default. Use `everos init --xdg` to
|
||||||
|
write `${XDG_CONFIG_HOME:-~/.config}/everos/.env` instead.
|
||||||
|
|
||||||
|
### 3. Start The Server
|
||||||
|
|
||||||
|
```bash
|
||||||
everos --help
|
everos --help
|
||||||
everos server start
|
everos server start
|
||||||
```
|
```
|
||||||
@ -86,10 +160,13 @@ everos server start
|
|||||||
`everos server start` searches for `.env` in this order: `--env-file <path>` →
|
`everos server start` searches for `.env` in this order: `--env-file <path>` →
|
||||||
`./.env` (cwd) → `${XDG_CONFIG_HOME:-~/.config}/everos/.env` → `~/.everos/.env`.
|
`./.env` (cwd) → `${XDG_CONFIG_HOME:-~/.config}/everos/.env` → `~/.everos/.env`.
|
||||||
The endpoint stack is OpenAI-protocol compatible (OpenAI / OpenRouter / vLLM /
|
The endpoint stack is OpenAI-protocol compatible (OpenAI / OpenRouter / vLLM /
|
||||||
Ollama / DeepInfra …) — override `*__BASE_URL` in the generated `.env` to point
|
Ollama / DeepInfra) - override `*__BASE_URL` in the generated `.env` to point
|
||||||
at any of them.
|
at any of them.
|
||||||
|
|
||||||
#### Multi-modal (optional)
|
For a step-by-step walkthrough (add a conversation, flush, search, then
|
||||||
|
read the markdown), see [QUICKSTART.md](QUICKSTART.md).
|
||||||
|
|
||||||
|
### Optional: Ingest Multimodal Files
|
||||||
|
|
||||||
To ingest non-text content (image / pdf / audio / office documents)
|
To ingest non-text content (image / pdf / audio / office documents)
|
||||||
through `/api/v1/memory/add` `content` items, install the optional
|
through `/api/v1/memory/add` `content` items, install the optional
|
||||||
@ -118,26 +195,53 @@ brew install --cask libreoffice # macOS
|
|||||||
sudo apt-get install -y libreoffice # Debian / Ubuntu
|
sudo apt-get install -y libreoffice # Debian / Ubuntu
|
||||||
```
|
```
|
||||||
|
|
||||||
For a step-by-step walkthrough (add a conversation → flush → search →
|
### For Contributors
|
||||||
read the markdown), see [QUICKSTART.md](QUICKSTART.md).
|
|
||||||
|
|
||||||
|
|
||||||
### Develop locally
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo>
|
git clone https://github.com/EverMind-AI/EverOS.git
|
||||||
cd everos
|
cd EverOS
|
||||||
uv sync # creates ./.venv and installs deps
|
uv sync # creates ./.venv and installs deps
|
||||||
source .venv/bin/activate # — or skip activation and prefix every command with `uv run`
|
source .venv/bin/activate # or prefix commands with `uv run`
|
||||||
everos init # fill in EVEROS_LLM__API_KEY in the generated .env
|
everos init # fill the four API key slots in .env (two distinct keys)
|
||||||
|
|
||||||
everos --help
|
everos --help
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
## Storage layout
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Architecture At A Glance
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ entrypoints/ (CLI + HTTP API) │ presentation
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ service/ (use cases: memorize/retrieve) │ application
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ memory/ (extract + search + cascade) │ domain
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ infra/ (markdown / sqlite / lancedb) │ infrastructure
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
|
↑ ↑
|
||||||
|
component/ core/
|
||||||
|
(LLM/Embedding) (observability/lifespan)
|
||||||
|
```
|
||||||
|
|
||||||
|
DDD 5 layers, single-direction dependency. See [docs/architecture.md](docs/architecture.md).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Storage Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.everos/
|
~/.everos/
|
||||||
@ -165,10 +269,15 @@ is the user-facing memory surface, while extracted derivatives sit
|
|||||||
quietly alongside.
|
quietly alongside.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Hybrid retrieval**: BM25 + vector (HNSW/IVF-PQ) + scalar filter, single-query in LanceDB
|
- **Hybrid retrieval**: BM25 + cosine vector ANN + scalar filters, backed by LanceDB
|
||||||
- **Cascade index sync**: edit a `.md` → file watcher → entry-level diff → LanceDB sync, sub-second
|
- **Cascade index sync**: edit a `.md` → file watcher → entry-level diff → LanceDB sync, sub-second
|
||||||
- **Multi-source extraction**: conversations / agent trajectories / file knowledge
|
- **Multi-source extraction**: conversations / agent trajectories / file knowledge
|
||||||
- **Dual-track memory**: user-track (Episodes / Profiles) + agent-track (Cases / Skills)
|
- **Dual-track memory**: user-track (Episodes / Profiles) + agent-track (Cases / Skills)
|
||||||
@ -176,8 +285,13 @@ quietly alongside.
|
|||||||
- **Multi-modal**: text + small image / audio inline; large media via S3/OSS reference
|
- **Multi-modal**: text + small image / audio inline; large media via S3/OSS reference
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
## Project structure
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
everos/ # repo root
|
everos/ # repo root
|
||||||
@ -194,21 +308,36 @@ everos/ # repo root
|
|||||||
└── .claude/ # team-shared rules + skills (auto-loaded by Claude Code)
|
└── .claude/ # team-shared rules + skills (auto-loaded by Claude Code)
|
||||||
```
|
```
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [docs/overview.md](docs/overview.md) — Project overview & vision
|
- [docs/overview.md](docs/overview.md) — Project overview & vision
|
||||||
- [docs/architecture.md](docs/architecture.md) — DDD layered architecture & dependency rules
|
- [docs/architecture.md](docs/architecture.md) — DDD layered architecture & dependency rules
|
||||||
- [docs/engineering.md](docs/engineering.md) — Engineering & dev-efficiency infrastructure (CI / tooling / Claude Code)
|
- [docs/engineering.md](docs/engineering.md) — Engineering & dev-efficiency infrastructure (CI / tooling / Claude Code)
|
||||||
|
- [docs/use-cases.md](docs/use-cases.md) — Full use-case gallery and integration examples
|
||||||
|
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) — Legacy API and infrastructure migration notes
|
||||||
- [CHANGELOG.md](CHANGELOG.md) — Release notes
|
- [CHANGELOG.md](CHANGELOG.md) — Release notes
|
||||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute
|
||||||
- [.claude/rules/](.claude/rules/) — Detailed coding conventions (auto-loaded by Claude Code)
|
- [.claude/rules/](.claude/rules/) — Detailed coding conventions (auto-loaded by Claude Code)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
Use cases show what persistent memory makes possible in real products and workflows. Some examples are packaged in this repository; others point to external demos or integrations you can study and adapt.
|
Use cases show what persistent memory makes possible in real products and
|
||||||
|
workflows. Some examples are packaged in this repository; others point to
|
||||||
|
external demos or integrations you can study and adapt.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -216,7 +345,7 @@ Use cases show what persistent memory makes possible in real products and workfl
|
|||||||
|
|
||||||
[](https://evermind.ai/usecase_reunite)
|
[](https://evermind.ai/usecase_reunite)
|
||||||
|
|
||||||
#### Reunite - Find with EverOS
|
#### Reunite - Find With EverOS
|
||||||
|
|
||||||
Parents describe what they remember. Children describe what they recall. Reunite uses semantic memory to surface the connections.
|
Parents describe what they remember. Children describe what they recall. Reunite uses semantic memory to surface the connections.
|
||||||
|
|
||||||
@ -229,7 +358,7 @@ Parents describe what they remember. Children describe what they recall. Reunite
|
|||||||
|
|
||||||
#### Hive Orchestrator
|
#### Hive Orchestrator
|
||||||
|
|
||||||
Browser-native hive-mind for CLI coding agents — Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.
|
Browser-native hive-mind for CLI coding agents - Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.
|
||||||
|
|
||||||
[Code](https://github.com/tt-a1i/hive)
|
[Code](https://github.com/tt-a1i/hive)
|
||||||
|
|
||||||
@ -241,7 +370,7 @@ Browser-native hive-mind for CLI coding agents — Claude Code, Codex, Gemini, a
|
|||||||
|
|
||||||
[](https://github.com/tt-a1i/evermemos-mcp)
|
[](https://github.com/tt-a1i/evermemos-mcp)
|
||||||
|
|
||||||
#### AI Coding Assistants with EverOS
|
#### AI Coding Assistants With EverOS
|
||||||
|
|
||||||
Universal long-term memory layer for AI coding assistants, powered by EverOS.
|
Universal long-term memory layer for AI coding assistants, powered by EverOS.
|
||||||
|
|
||||||
@ -252,9 +381,9 @@ Universal long-term memory layer for AI coding assistants, powered by EverOS.
|
|||||||
|
|
||||||
[](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
[](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
#### AI Data Techician
|
#### AI Data Technician
|
||||||
|
|
||||||
An agentic AI system that learns from scientist interaction to inspect, analyze, and classify high-dimensional time series data — with persistent memory that improves across sessions.
|
An agentic AI system that learns from scientist interaction to inspect, analyze, and classify high-dimensional time series data - with persistent memory that improves across sessions.
|
||||||
|
|
||||||
[Code](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
[Code](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
@ -266,7 +395,7 @@ An agentic AI system that learns from scientist interaction to inspect, analyze,
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Rokid AI Assistant with EverOS
|
#### Rokid AI Assistant With EverOS
|
||||||
|
|
||||||
Connect to EverOS within Rokid Glasses enabling long-term memory for all of your smart activities.
|
Connect to EverOS within Rokid Glasses enabling long-term memory for all of your smart activities.
|
||||||
|
|
||||||
@ -277,15 +406,21 @@ Coming soon
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Creative Assistant with Memory
|
#### Creative Assistant With Memory
|
||||||
|
|
||||||
Creative assistant with long-term memory, never forget your crativites anymore.
|
Creative assistant with long-term memory, so your creative context stays available across sessions.
|
||||||
|
|
||||||
Coming soon
|
Coming soon
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
@ -300,7 +435,7 @@ Earth Online is a memory-aware productivity game that turns everyday planning in
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/golutra/golutra)
|
[](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
#### Multi-Agent Orchestration Platform
|
#### Multi-Agent Orchestration Platform
|
||||||
|
|
||||||
@ -324,11 +459,11 @@ Record, visualize, and explore your tasting journey through an immersive 3D star
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/kellyvv/OpenHer)
|
[](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
#### EverOS Open Her
|
#### EverOS Open Her
|
||||||
|
|
||||||
Build AI that feels. Open-source persona engine — personality emerges from neural drives, not prompts. Inspired by Her.
|
Build AI that feels. Open-source persona engine - personality emerges from neural drives, not prompts. Inspired by Her.
|
||||||
|
|
||||||
[Code](https://github.com/kellyvv/OpenHer)
|
[Code](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
@ -340,7 +475,7 @@ Build AI that feels. Open-source persona engine — personality emerges from neu
|
|||||||
|
|
||||||
[](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
[](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
||||||
|
|
||||||
#### Browser Agent for Personal Memory
|
#### Browser Agent For Personal Memory
|
||||||
|
|
||||||
Ruminer brings persistent memory to a browser agent so it can carry personal context across web tasks.
|
Ruminer brings persistent memory to a browser agent so it can carry personal context across web tasks.
|
||||||
|
|
||||||
@ -349,9 +484,9 @@ Ruminer brings persistent memory to a browser agent so it can carry personal con
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/nanxingw/EverMem)
|
[](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
#### EverMem Sync with EverOS
|
#### EverMem Sync With EverOS
|
||||||
|
|
||||||
One command to connect any AI coding CLI to EverMemOS long-term memory.
|
One command to connect any AI coding CLI to EverMemOS long-term memory.
|
||||||
|
|
||||||
@ -360,6 +495,12 @@ One command to connect any AI coding CLI to EverMemOS long-term memory.
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
@ -374,9 +515,9 @@ MCO equips your primary agent with an agent team that can work together to solve
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/onenewborn/StudyBuddy-public)
|
[](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
#### Study Buddy with Self-Evolving Memory
|
#### Study Buddy With Self-Evolving Memory
|
||||||
|
|
||||||
Study proactively with an agent that has self-evolving memory.
|
Study proactively with an agent that has self-evolving memory.
|
||||||
|
|
||||||
@ -390,7 +531,7 @@ Study proactively with an agent that has self-evolving memory.
|
|||||||
|
|
||||||
[](https://github.com/TonyLiangDesign/MemoCare)
|
[](https://github.com/TonyLiangDesign/MemoCare)
|
||||||
|
|
||||||
#### Alzheimer’s Memory Assistant
|
#### Alzheimer's Memory Assistant
|
||||||
|
|
||||||
Empowering individuals with advanced memory support and daily assistance.
|
Empowering individuals with advanced memory support and daily assistance.
|
||||||
|
|
||||||
@ -399,7 +540,7 @@ Empowering individuals with advanced memory support and daily assistance.
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/AlexL1024/NeuralConnect)
|
[](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
#### Memory-Driven Multi-Agent NPC Experience
|
#### Memory-Driven Multi-Agent NPC Experience
|
||||||
|
|
||||||
@ -426,31 +567,37 @@ An iOS app where users create, nurture, and live with a personalized AI companio
|
|||||||
|
|
||||||
[](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
[](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
#### AI Wearable with Memory
|
#### AI Wearable With Memory
|
||||||
|
|
||||||
A context-native AI wearable that listens to everyday life and converts conversations into memory.
|
A context-native AI wearable that listens to everyday life and converts conversations into memory.
|
||||||
|
|
||||||
[Code](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
[Code](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/EverMind-AI/EverOS/tree/0f49826ba0f9a94e1974c97614a46a68e0a08b52/evermemos-openclaw-plugin)
|
[](docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
#### OpenClaw Agent Memory
|
#### Legacy OpenClaw Agent Memory
|
||||||
|
|
||||||
A 24/7 agent workflow with continuous learning memory across sessions.
|
Archived pre-1.0.0 plugin reference. New integrations should use the EverOS 1.0.0 API.
|
||||||
|
|
||||||
[Plugin](https://github.com/EverMind-AI/EverOS/tree/0f49826ba0f9a94e1974c97614a46a68e0a08b52/evermemos-openclaw-plugin)
|
[Learn more](docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
[](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
||||||
|
|
||||||
#### Live2D Character with Memory
|
#### Live2D Character With Memory
|
||||||
|
|
||||||
Add long-term memory to a real-time Live2D character, powered by [TEN Framework](https://github.com/TEN-framework/ten-framework).
|
Add long-term memory to a real-time Live2D character, powered by [TEN Framework](https://github.com/TEN-framework/ten-framework).
|
||||||
|
|
||||||
@ -463,7 +610,7 @@ Add long-term memory to a real-time Live2D character, powered by [TEN Framework]
|
|||||||
|
|
||||||
[](https://screenshot-analysis-vercel.vercel.app/)
|
[](https://screenshot-analysis-vercel.vercel.app/)
|
||||||
|
|
||||||
#### Computer-Use with Memory
|
#### Computer-Use With Memory
|
||||||
|
|
||||||
Run screenshot-based analysis with computer-use and store the results in memory.
|
Run screenshot-based analysis with computer-use and store the results in memory.
|
||||||
|
|
||||||
@ -474,7 +621,7 @@ Run screenshot-based analysis with computer-use and store the results in memory.
|
|||||||
|
|
||||||
[](use-cases/game-of-throne-demo)
|
[](use-cases/game-of-throne-demo)
|
||||||
|
|
||||||
#### Game of Thrones Memories
|
#### Game Of Thrones Memories
|
||||||
|
|
||||||
A demonstration of AI memory infrastructure through an interactive Q&A experience with *A Game of Thrones*.
|
A demonstration of AI memory infrastructure through an interactive Q&A experience with *A Game of Thrones*.
|
||||||
|
|
||||||
@ -515,11 +662,19 @@ Explore stored entities and relationships in a graph interface. Frontend demo; b
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Stay Tuned
|
## Watch EverOS
|
||||||
|
|
||||||
Star the repo or join the community links above to follow new architecture methods, benchmark releases, and memory-enabled use cases.
|
EverOS 1.0.0 is the first release of a larger memory-system roadmap.
|
||||||
|
Watch this repository for upcoming work on Wiki-style memory, Dreaming,
|
||||||
|
deeper offline evolution, benchmark releases, and more real-world agent
|
||||||
|
integrations.
|
||||||
|
|
||||||

|
If EverOS is useful to your agent stack, starring the repo helps more
|
||||||
|
builders discover it.
|
||||||
|
|
||||||
|
### Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#EverMind-AI/EverOS&Date)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div align="right">
|
<div align="right">
|
||||||
@ -528,6 +683,55 @@ Star the repo or join the community links above to follow new architecture metho
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## EverMind Ecosystems
|
||||||
|
|
||||||
|
EverMind is an open-source ecosystem for long-term memory, self-evolving agents, and memory evaluation.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">EverMind Open-Source Ecosystem</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Core Memory Architecture</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverOS">EverOS</a> - the local memory operating system and research-backed runtime for agent and user memory.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Algorithm Engine</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverAlgo">EverAlgo</a> - stateless extraction, ranking, parsing, and memory operators that power EverOS.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Alternative Architecture</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/HyperMem">HyperMem</a> - hypergraph memory for long-term conversations, with its own benchmark-backed topic -> episode -> fact retrieval method.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Benchmarks</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverMemBench">EverMemBench</a> · <a href="https://github.com/EverMind-AI/EvoAgentBench">EvoAgentBench</a> - evaluation suites for conversational memory and agent self-evolution.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Long-Context Research</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/MSA">MSA</a> - Memory Sparse Attention for scalable latent memory and 100M-token contexts.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Personal Memory Layer</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverMe">EverMe</a> - CLI and agent plugin suite for cross-device, cross-agent personal memory.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Developer Integrations</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/evermem-claude-code">evermem-claude-code</a> · <a href="https://github.com/EverMind-AI/everos-plugins">everos-plugins</a> - plugins, skills, and migration tooling for AI coding agents.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Together, these repositories form EverMind's research-to-runtime stack: new memory methods, reusable algorithms, benchmark evidence, and practical agent integrations.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome across the whole repository: architecture methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
|
Contributions are welcome across the whole repository: architecture methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
|
||||||
|
|||||||
764
README.zh-CN.md
Normal file
764
README.zh-CN.md
Normal file
@ -0,0 +1,764 @@
|
|||||||
|
<div align="center" id="readme-top">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://x.com/evermind"><img src="https://img.shields.io/badge/EverMind-000000?labelColor=gray&style=for-the-badge&logo=x&logoColor=white" alt="X"></a>
|
||||||
|
<a href="https://huggingface.co/EverMind-AI"><img src="https://img.shields.io/badge/🤗_HuggingFace-EverMind-F5C842?labelColor=gray&style=for-the-badge" alt="HuggingFace"></a>
|
||||||
|
<a href="https://discord.gg/gYep5nQRZJ"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Fv10%2Finvites%2FgYep5nQRZJ%3Fwith_counts%3Dtrue&query=%24.approximate_presence_count&suffix=%20online&label=Discord&color=404EED&labelColor=gray&style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
|
||||||
|
<a href="https://github.com/EverMind-AI/EverOS/discussions/67"><img src="https://img.shields.io/badge/WeCom-EverMind_社区-07C160?labelColor=gray&style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[官网](https://evermind.ai) · [文档](https://docs.evermind.ai) · [博客](https://evermind.ai/blogs) · [English](README.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><kbd>目录</kbd></summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
- [EverOS 1.0.0 亮点](#everos-100-亮点)
|
||||||
|
- [为什么选择 EverOS](#为什么选择-everos)
|
||||||
|
- [快速开始](#快速开始)
|
||||||
|
- [架构概览](#架构概览)
|
||||||
|
- [存储布局](#存储布局)
|
||||||
|
- [功能](#功能)
|
||||||
|
- [项目结构](#项目结构)
|
||||||
|
- [文档](#文档)
|
||||||
|
- [使用场景](#使用场景)
|
||||||
|
- [关注 EverOS](#关注-everos)
|
||||||
|
- [EverMind 生态](#evermind-生态)
|
||||||
|
- [参与贡献](#参与贡献)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## EverOS 1.0.0 亮点
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> **EverOS 1.0.0 是面向自进化记忆的一次重要发布。** 它带来了
|
||||||
|
> local-first 运行时、Markdown 作为 source of truth、混合检索、
|
||||||
|
> 多模态摄取、用户记忆与 Agent 记忆作用域,以及由
|
||||||
|
> [EverAlgo](https://github.com/EverMind-AI/EverAlgo) 支撑的模块化算法。
|
||||||
|
>
|
||||||
|
> **欢迎 Watch 这个仓库。** 下一阶段我们会继续推进记忆系统方法,
|
||||||
|
> 包括 Wiki 式知识层和用于更深层离线进化的 Dreaming。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Markdown As Source Of Truth</strong><br>
|
||||||
|
<br>
|
||||||
|
所有记忆持久化为 <code>.md</code> 文件:可读、可改、可 grep、可 Git 版本化,也可直接用 Obsidian 打开。
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Local Three-Part Stack</strong><br>
|
||||||
|
<br>
|
||||||
|
Markdown + SQLite + LanceDB 在本地完成向量、BM25 和标量过滤检索,无需 MongoDB、Elasticsearch 或 Redis。
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Dual-Track Memory</strong><br>
|
||||||
|
<br>
|
||||||
|
Agent 记忆(<code>cases</code> / <code>skills</code>)与用户记忆(<code>episodes</code> / <code>profile</code>)独立提取,互不污染。
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Multimodal Ingestion</strong><br>
|
||||||
|
<br>
|
||||||
|
文本、图像、音频、文档、PDF、HTML 和邮件统一抽取为可检索的记忆形态。
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Self-Evolution</strong><br>
|
||||||
|
<br>
|
||||||
|
从真实使用经验中自动抽取共性 skills,重复模式沉淀为可复用流程,无需重训。
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top">
|
||||||
|
<strong>Orthogonal Retrieval</strong><br>
|
||||||
|
<br>
|
||||||
|
按 <code>user_id</code>、<code>agent_id</code>、<code>app_id</code>、<code>project_id</code> 和 <code>session_id</code> 五维独立检索。
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 为什么选择 EverOS
|
||||||
|
|
||||||
|
EverOS 是一个开源 Python 框架,用来构建**跨 Agent、跨平台的自进化长期记忆**。
|
||||||
|
它为 maker 提供一层可携带的统一记忆层,适用于他们使用的每一个 Agent:
|
||||||
|
Claude Code、Codex、OpenClaw、Hermes 等等。这样,上下文、决策、文件和
|
||||||
|
Agent 轨迹可以跟着工作流走,而不是被锁在某一个工具里。
|
||||||
|
|
||||||
|
EverOS 会把对话、Agent 轨迹和文件保存为可读 Markdown,并同步本地 SQLite
|
||||||
|
和 LanceDB 索引,以便快速检索。Agent 可以复用过去的 cases 和 skills,从重复
|
||||||
|
工作流中自我改进,并逐渐变得更加主动。
|
||||||
|
|
||||||
|
系统围绕三个边界设计:
|
||||||
|
|
||||||
|
1. **记忆内容保持可读** - Markdown 是长期、耐用的 source of truth。
|
||||||
|
2. **运行时状态保持本地** - SQLite 跟踪状态;LanceDB 处理向量、BM25 和结构化过滤搜索。
|
||||||
|
3. **算法保持模块化** - [EverAlgo](https://github.com/EverMind-AI/EverAlgo) 负责记忆算法;EverOS 负责运行时、持久化、在线流程和离线进化。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装 EverOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install everos
|
||||||
|
# or: pip install everos
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 初始化配置
|
||||||
|
|
||||||
|
生成一个 starter `.env` 文件,然后根据生成的注释填入 API key 字段。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
everos init
|
||||||
|
```
|
||||||
|
|
||||||
|
`everos init` 默认写入 `./.env`。也可以使用 `everos init --xdg`
|
||||||
|
写入 `${XDG_CONFIG_HOME:-~/.config}/everos/.env`。
|
||||||
|
|
||||||
|
### 3. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
everos --help
|
||||||
|
everos server start
|
||||||
|
```
|
||||||
|
|
||||||
|
`everos server start` 会按以下顺序查找 `.env`:`--env-file <path>` →
|
||||||
|
`./.env`(当前目录)→ `${XDG_CONFIG_HOME:-~/.config}/everos/.env` →
|
||||||
|
`~/.everos/.env`。端点栈兼容 OpenAI protocol(OpenAI / OpenRouter /
|
||||||
|
vLLM / Ollama / DeepInfra)。你可以覆盖生成的 `.env` 中的 `*__BASE_URL`
|
||||||
|
来指向任意这些模型服务。
|
||||||
|
|
||||||
|
完整 walkthrough(添加对话、flush、search,然后读取 Markdown)见
|
||||||
|
[QUICKSTART.md](QUICKSTART.md)。
|
||||||
|
|
||||||
|
### 可选:摄取多模态文件
|
||||||
|
|
||||||
|
如果要通过 `/api/v1/memory/add` 的 `content` items 摄取非文本内容
|
||||||
|
(image / pdf / audio / office documents),安装可选 extra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install 'everos[multimodal]' # or: pip install 'everos[multimodal]'
|
||||||
|
```
|
||||||
|
|
||||||
|
这会引入 `everalgo-parser`(包含用于 SVG 支持的 `[svg]` bundle,通过
|
||||||
|
cairosvg)并接入多模态 LLM client(`.env` 中的 `EVEROS_MULTIMODAL__*`
|
||||||
|
字段,默认通过 OpenRouter 使用 `google/gemini-3-flash-preview`)。
|
||||||
|
|
||||||
|
**Office 文档支持需要 LibreOffice 作为系统依赖。** parser 会调用
|
||||||
|
`soffice`(LibreOffice 的 headless renderer),先把 `.doc` / `.docx` /
|
||||||
|
`.ppt` / `.pptx` / `.xls` / `.xlsx` 转换为 PDF,再交给多模态 LLM。
|
||||||
|
如果没有 LibreOffice,office 上传会返回 HTTP 415,并带有明确错误信息;
|
||||||
|
PDF / image / audio / HTML / email 解析不受影响。
|
||||||
|
|
||||||
|
在提供 office 文档服务前,请先在宿主机安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install --cask libreoffice # macOS
|
||||||
|
sudo apt-get install -y libreoffice # Debian / Ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
### 贡献者开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/EverMind-AI/EverOS.git
|
||||||
|
cd EverOS
|
||||||
|
uv sync # creates ./.venv and installs deps
|
||||||
|
source .venv/bin/activate # or prefix commands with `uv run`
|
||||||
|
everos init # fill the four API key slots in .env (two distinct keys)
|
||||||
|
|
||||||
|
everos --help
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ entrypoints/ (CLI + HTTP API) │ presentation
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ service/ (use cases: memorize/retrieve) │ application
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ memory/ (extract + search + cascade) │ domain
|
||||||
|
├───────────────────────────────────────────────┤
|
||||||
|
│ infra/ (markdown / sqlite / lancedb) │ infrastructure
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
|
↑ ↑
|
||||||
|
component/ core/
|
||||||
|
(LLM/Embedding) (observability/lifespan)
|
||||||
|
```
|
||||||
|
|
||||||
|
DDD 5 层架构,单向依赖。详见 [docs/architecture.md](docs/architecture.md)。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 存储布局
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.everos/
|
||||||
|
├── default_app/ # app_id ("default" → "default_app" on disk)
|
||||||
|
│ └── default_project/ # project_id ("default" → "default_project")
|
||||||
|
│ ├── users/<user_id>/
|
||||||
|
│ │ ├── user.md # profile
|
||||||
|
│ │ ├── episodes/ # daily-log episodes (visible)
|
||||||
|
│ │ ├── .atomic_facts/ # nested facts (dotfile-hidden)
|
||||||
|
│ │ └── .foresights/ # predictive memory (dotfile-hidden)
|
||||||
|
│ └── agents/<agent_id>/
|
||||||
|
│ ├── agent.md
|
||||||
|
│ ├── .cases/ # one task case per entry
|
||||||
|
│ └── skills/ # named procedural memories
|
||||||
|
├── .index/ # derived indexes (rebuildable from md)
|
||||||
|
│ ├── sqlite/system.db # state + queue + audit
|
||||||
|
│ └── lancedb/*.lance/ # vector + BM25 + scalar
|
||||||
|
└── .tmp/ # transient working files
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Obsidian 中打开任意 `<app>/<project>/users/<user_id>/` 文件夹即可。
|
||||||
|
你的 Agent 大脑本质上就是一组文件。dotfile 目录(`.atomic_facts/`、
|
||||||
|
`.foresights/`、`.cases/`)默认保持隐藏,因此可见文件夹仍然是面向用户的
|
||||||
|
记忆表面,而提取出的衍生信息则安静地放在旁边。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- **混合检索**: BM25 + vector(HNSW/IVF-PQ)+ scalar filter,在 LanceDB 中完成单次查询
|
||||||
|
- **级联索引同步**: 编辑 `.md` → file watcher → entry-level diff → LanceDB sync,亚秒级同步
|
||||||
|
- **多源提取**: conversations / agent trajectories / file knowledge
|
||||||
|
- **双轨记忆**: user-track(Episodes / Profiles)+ agent-track(Cases / Skills)
|
||||||
|
- **异步优先**: 完整 asyncio,单一 event loop
|
||||||
|
- **多模态**: text + 小图片 / audio inline;大媒体通过 S3/OSS reference
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
everos/ # repo root
|
||||||
|
├── src/everos/ # main package (src layout)
|
||||||
|
│ ├── entrypoints/ # cli + api
|
||||||
|
│ ├── service/ # use case orchestration
|
||||||
|
│ ├── memory/ # domain: extract + search + cascade + prompt_slots
|
||||||
|
│ ├── infra/ # storage: markdown + lancedb + sqlite
|
||||||
|
│ ├── component/ # cross-cutting: llm / embedding / config / utils
|
||||||
|
│ ├── core/ # runtime: observability / lifespan / context
|
||||||
|
│ └── config/ # configuration data + Settings schema
|
||||||
|
├── tests/ # unit / integration / golden / fixtures
|
||||||
|
├── docs/ # design docs
|
||||||
|
└── .claude/ # team-shared rules + skills (auto-loaded by Claude Code)
|
||||||
|
```
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [docs/overview.md](docs/overview.md) - 项目概览与愿景
|
||||||
|
- [docs/architecture.md](docs/architecture.md) - DDD 分层架构与依赖规则
|
||||||
|
- [docs/engineering.md](docs/engineering.md) - 工程与开发效率基础设施(CI / tooling / Claude Code)
|
||||||
|
- [docs/use-cases.md](docs/use-cases.md) - 完整使用场景 gallery 和集成示例
|
||||||
|
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) - Legacy API 与基础设施迁移说明
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) - 发布记录
|
||||||
|
- [CONTRIBUTING.md](CONTRIBUTING.md) - 如何贡献
|
||||||
|
- [.claude/rules/](.claude/rules/) - 详细代码规范(Claude Code 会自动加载)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
这些使用场景展示了持久记忆可以在真实产品和工作流中带来什么能力。
|
||||||
|
有些示例已经打包在本仓库中,另一些则指向外部 demo 或集成,你可以研究并复用。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://evermind.ai/usecase_reunite)
|
||||||
|
|
||||||
|
#### Reunite - 用 EverOS 找回连接
|
||||||
|
|
||||||
|
父母描述他们记得的线索,孩子描述他们残留的回忆。Reunite 使用语义记忆来浮现这些连接。
|
||||||
|
|
||||||
|
[了解更多](https://evermind.ai/usecase_reunite)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/tt-a1i/hive)
|
||||||
|
|
||||||
|
#### Hive Orchestrator
|
||||||
|
|
||||||
|
面向 CLI coding agents 的 browser-native hive-mind。Claude Code、Codex、Gemini 和 OpenCode 作为真实 PTY 进程,通过团队协议协作。
|
||||||
|
|
||||||
|
[代码](https://github.com/tt-a1i/hive)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/tt-a1i/evermemos-mcp)
|
||||||
|
|
||||||
|
#### 接入 EverOS 的 AI 编程助手
|
||||||
|
|
||||||
|
由 EverOS 驱动的通用长期记忆层,面向 AI coding assistants。
|
||||||
|
|
||||||
|
[代码](https://github.com/tt-a1i/evermemos-mcp)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
|
#### AI Data Technician
|
||||||
|
|
||||||
|
一个 agentic AI 系统,可以从科学家的交互中学习,用于检查、分析和分类高维时间序列数据,并通过跨 session 改进的持久记忆持续变强。
|
||||||
|
|
||||||
|
[代码](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 接入 EverOS 的 Rokid AI 助手
|
||||||
|
|
||||||
|
在 Rokid Glasses 中连接 EverOS,为你的智能活动启用长期记忆。
|
||||||
|
|
||||||
|
即将推出
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 带长期记忆的创意助手
|
||||||
|
|
||||||
|
拥有长期记忆的创意助手,让你的创作上下文可以跨 session 持续可用。
|
||||||
|
|
||||||
|
即将推出
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/xunyud/Earth-Online)
|
||||||
|
|
||||||
|
#### Earth Online 记忆游戏
|
||||||
|
|
||||||
|
Earth Online 是一款 memory-aware productivity game,把日常计划变成一个持续生长的 quest log。
|
||||||
|
|
||||||
|
[代码](https://github.com/xunyud/Earth-Online)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
|
#### 多 Agent 编排平台
|
||||||
|
|
||||||
|
Golutra 为工程团队提供 multi-agent workforce,把 IDE 从单一 assistant 扩展为协同 agents。
|
||||||
|
|
||||||
|
[代码](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/Yangtze-Seventh/taste-verse)
|
||||||
|
|
||||||
|
#### 你的个人品鉴宇宙
|
||||||
|
|
||||||
|
通过沉浸式 3D 星图记录、可视化并探索你的 tasting journey。
|
||||||
|
|
||||||
|
[代码](https://github.com/Yangtze-Seventh/taste-verse)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
|
#### EverOS Open Her
|
||||||
|
|
||||||
|
构建有感受的 AI。开源 persona engine,让 personality 从 neural drives 中涌现,而不是来自 prompts。灵感来自 Her。
|
||||||
|
|
||||||
|
[代码](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
||||||
|
|
||||||
|
#### 面向个人记忆的浏览器 Agent
|
||||||
|
|
||||||
|
Ruminer 为 browser agent 带来持久记忆,让它能在不同网页任务之间携带个人上下文。
|
||||||
|
|
||||||
|
[插件](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
|
#### EverMem 与 EverOS 同步
|
||||||
|
|
||||||
|
一条命令,把任意 AI coding CLI 连接到 EverMemOS 长期记忆。
|
||||||
|
|
||||||
|
[代码](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/mco-org/mco)
|
||||||
|
|
||||||
|
#### MCO - 编排 AI Coding Agents
|
||||||
|
|
||||||
|
MCO 为你的主 Agent 配备一个 agent team,让它们可以一起处理复杂任务。
|
||||||
|
|
||||||
|
[代码](https://github.com/mco-org/mco)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
|
#### 带自进化记忆的 Study Buddy
|
||||||
|
|
||||||
|
使用拥有 self-evolving memory 的 Agent,主动辅助学习。
|
||||||
|
|
||||||
|
[代码](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/TonyLiangDesign/MemoCare)
|
||||||
|
|
||||||
|
#### 阿尔茨海默症记忆助手
|
||||||
|
|
||||||
|
通过高级记忆支持和日常辅助,帮助有需要的人更好地生活。
|
||||||
|
|
||||||
|
[代码](https://github.com/TonyLiangDesign/MemoCare)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
|
#### 记忆驱动的 Multi-Agent NPC 体验
|
||||||
|
|
||||||
|
一款 iOS 科幻悬疑游戏,玩家可以探索世界并揭开真相。
|
||||||
|
|
||||||
|
[代码](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/elontusk5219-prog/Mobi)
|
||||||
|
|
||||||
|
#### Mobi Companion
|
||||||
|
|
||||||
|
一款 iOS app,用户可以创建、养成并与名为 Mobi 的个性化 AI companion 一起生活。
|
||||||
|
|
||||||
|
[代码](https://github.com/elontusk5219-prog/Mobi)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
|
#### 带记忆的 AI 可穿戴设备
|
||||||
|
|
||||||
|
一个 context-native AI wearable,聆听日常生活,并把对话转换为记忆。
|
||||||
|
|
||||||
|
[代码](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" align="right">
|
||||||
|
<a href="#readme-top"><img src="https://img.shields.io/badge/-Back_to_top-gray?style=flat-square" alt="Back to top"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
|
#### Legacy OpenClaw Agent 记忆
|
||||||
|
|
||||||
|
已归档的 pre-1.0.0 plugin reference。新的集成应使用 EverOS 1.0.0 API。
|
||||||
|
|
||||||
|
[了解更多](docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
||||||
|
|
||||||
|
#### 带记忆的 Live2D 角色
|
||||||
|
|
||||||
|
为实时 Live2D character 添加长期记忆,由 [TEN Framework](https://github.com/TEN-framework/ten-framework) 驱动。
|
||||||
|
|
||||||
|
[代码](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://screenshot-analysis-vercel.vercel.app/)
|
||||||
|
|
||||||
|
#### 带记忆的 Computer-Use
|
||||||
|
|
||||||
|
运行基于截图的分析任务,并把结果存入记忆。
|
||||||
|
|
||||||
|
[在线演示](https://screenshot-analysis-vercel.vercel.app/)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](use-cases/game-of-throne-demo)
|
||||||
|
|
||||||
|
#### Game Of Thrones Memories
|
||||||
|
|
||||||
|
通过与 *A Game of Thrones* 互动问答体验,展示 AI 记忆基础设施。
|
||||||
|
|
||||||
|
[代码](use-cases/game-of-throne-demo)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](use-cases/claude-code-plugin)
|
||||||
|
|
||||||
|
#### Claude Code Plugin
|
||||||
|
|
||||||
|
Claude Code 的持久记忆插件。自动保存并回忆过去 coding sessions 的上下文。
|
||||||
|
|
||||||
|
[代码](use-cases/claude-code-plugin)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://main.d2j21qxnymu6wl.amplifyapp.com/graph.html)
|
||||||
|
|
||||||
|
#### 记忆图谱可视化
|
||||||
|
|
||||||
|
在图界面中探索已存储的 entities 和 relationships。前端 demo 已可用;后端集成仍在进行中。
|
||||||
|
|
||||||
|
[在线演示](https://main.d2j21qxnymu6wl.amplifyapp.com/graph.html)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 关注 EverOS
|
||||||
|
|
||||||
|
EverOS 1.0.0 是更大规模记忆系统路线图的第一个发布版本。Watch 这个仓库,
|
||||||
|
即可持续关注 Wiki 式记忆、Dreaming、更深入的离线进化、benchmark releases,
|
||||||
|
以及更多真实 Agent 集成。
|
||||||
|
|
||||||
|
如果 EverOS 对你的 Agent stack 有帮助,Star 这个仓库也会帮助更多 builders
|
||||||
|
发现它。
|
||||||
|
|
||||||
|
### Star 趋势
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#EverMind-AI/EverOS&Date)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## EverMind 生态
|
||||||
|
|
||||||
|
EverMind 是一个面向长期记忆、自进化 Agent 和记忆评测的开源生态。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">EverMind 开源生态</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>核心记忆架构</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverOS">EverOS</a> - 本地记忆操作系统,以及有研究支撑的 Agent 和用户记忆运行时。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>算法引擎</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverAlgo">EverAlgo</a> - stateless extraction、ranking、parsing 和 memory operators,为 EverOS 提供算法能力。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>替代架构</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/HyperMem">HyperMem</a> - 面向长期对话的 hypergraph memory,拥有独立的 benchmark-backed topic -> episode -> fact 检索方法。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Benchmarks</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverMemBench">EverMemBench</a> · <a href="https://github.com/EverMind-AI/EvoAgentBench">EvoAgentBench</a> - conversational memory 和 Agent self-evolution 的评测套件。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Long-Context Research</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/MSA">MSA</a> - Memory Sparse Attention,用于可扩展 latent memory 和 100M-token contexts。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>个人记忆层</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/EverMe">EverMe</a> - CLI 和 Agent plugin suite,用于跨设备、跨 Agent 的个人记忆。</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>开发者集成</strong></td>
|
||||||
|
<td><a href="https://github.com/EverMind-AI/evermem-claude-code">evermem-claude-code</a> · <a href="https://github.com/EverMind-AI/everos-plugins">everos-plugins</a> - AI coding agents 的 plugins、skills 和 migration tooling。</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
这些仓库共同构成 EverMind 的 research-to-runtime stack:新的记忆方法、可复用算法、
|
||||||
|
benchmark evidence,以及可落地的 Agent 集成。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 参与贡献
|
||||||
|
|
||||||
|
欢迎为整个仓库贡献:架构方法、benchmark coverage、use-case examples、文档和 bug fixes。
|
||||||
|
浏览 [Issues](https://github.com/EverMind-AI/EverOS/issues) 找到适合的切入点,
|
||||||
|
准备好后即可提交 PR。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> **欢迎各种形式的贡献** 🎉
|
||||||
|
>
|
||||||
|
> 一起让 EverOS 变得更好。代码、文档、benchmark reports、use-case write-ups
|
||||||
|
> 和 integration examples 都很有价值。也欢迎在社交媒体上分享你的项目,启发更多人。
|
||||||
|
>
|
||||||
|
> 你可以在 𝕏 上联系 EverOS maintainer [@elliotchen200](https://x.com/elliotchen200),
|
||||||
|
> 或在 GitHub 上联系 [@cyfyifanchen](https://github.com/cyfyifanchen),获取项目更新、
|
||||||
|
> 讨论和协作机会。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### 代码贡献者
|
||||||
|
|
||||||
|
[](https://github.com/EverMind-AI/EverOS/graphs/contributors)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### 许可证
|
||||||
|
|
||||||
|
[Apache License 2.0](LICENSE) - 第三方归属说明请见 [NOTICE](NOTICE)。
|
||||||
|
|
||||||
|
### 引用
|
||||||
|
|
||||||
|
如果你在研究中使用 EverOS,请参考 [CITATION.md](CITATION.md)。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div align="right">
|
||||||
|
|
||||||
|
[](#readme-top)
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -24,6 +24,17 @@
|
|||||||
model = "gpt-4o-mini"
|
model = "gpt-4o-mini"
|
||||||
api_key = "sk-..."
|
api_key = "sk-..."
|
||||||
base_url = "https://api.openai.com/v1"
|
base_url = "https://api.openai.com/v1"
|
||||||
|
timeout_seconds = 180.0
|
||||||
|
|
||||||
|
# ── Multimodal LLM ───────────────────────────────────
|
||||||
|
# Independent vision/audio-capable chat-completions endpoint for parsing.
|
||||||
|
[multimodal]
|
||||||
|
model = "google/gemini-3-flash-preview"
|
||||||
|
api_key = "sk-..."
|
||||||
|
base_url = "https://openrouter.ai/api/v1"
|
||||||
|
timeout_seconds = 180.0
|
||||||
|
resize_images_for_vlm = true
|
||||||
|
max_concurrency = 4
|
||||||
|
|
||||||
# ── Embedding ─────────────────────────────────────────
|
# ── Embedding ─────────────────────────────────────────
|
||||||
[embedding]
|
[embedding]
|
||||||
|
|||||||
@ -793,7 +793,7 @@ attribution, so `session_id` is the only meaningful query dimension.
|
|||||||
| `sender_id` | `string` | Original sender id from `/add` |
|
| `sender_id` | `string` | Original sender id from `/add` |
|
||||||
| `sender_name` | `string \| null` | Original sender name; `null` if not provided |
|
| `sender_name` | `string \| null` | Original sender name; `null` if not provided |
|
||||||
| `role` | `"user" \| "assistant" \| "tool"` | Original role |
|
| `role` | `"user" \| "assistant" \| "tool"` | Original role |
|
||||||
| `content` | `string \| array<object>` | `string` for the single-text shorthand, `array` of opaque content items for the original multimodal payload (mirrors [MessageItem.content](#addmessage)) |
|
| `content` | `string \| array<object>` | `string` for the single-text shorthand, `array` of opaque content items for the original multimodal payload (mirrors [MessageItem.content](#messageitem)) |
|
||||||
| `timestamp` | `string` | ISO-8601 with timezone offset — see [Conventions](#conventions) |
|
| `timestamp` | `string` | ISO-8601 with timezone offset — see [Conventions](#conventions) |
|
||||||
| `tool_calls` | `array<object> \| null` | Original tool_calls payload if any |
|
| `tool_calls` | `array<object> \| null` | Original tool_calls payload if any |
|
||||||
| `tool_call_id` | `string \| null` | Original tool_call_id if any |
|
| `tool_call_id` | `string \| null` | Original tool_call_id if any |
|
||||||
|
|||||||
@ -204,7 +204,7 @@ everalgo is:
|
|||||||
- **No I/O** — does not touch md files / LanceDB / SQLite
|
- **No I/O** — does not touch md files / LanceDB / SQLite
|
||||||
- **No prompts inline** — receives `PromptSlot` parameter, project supplies defaults
|
- **No prompts inline** — receives `PromptSlot` parameter, project supplies defaults
|
||||||
|
|
||||||
This boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).
|
This boundary lets everalgo be reused across product forms (this open-source build, EverMind Cloud, OpenClaw plugins, etc.).
|
||||||
|
|
||||||
## Further reading
|
## Further reading
|
||||||
|
|
||||||
|
|||||||
@ -204,7 +204,7 @@ Lives in `LanceDBSettings`; overridable via the
|
|||||||
`EVEROS_LANCEDB__INDEX_CACHE_SIZE_BYTES` environment variable. This
|
`EVEROS_LANCEDB__INDEX_CACHE_SIZE_BYTES` environment variable. This
|
||||||
is the only knob that bounds the steady-state file-descriptor count
|
is the only knob that bounds the steady-state file-descriptor count
|
||||||
of a long-running EverOS daemon — see
|
of a long-running EverOS daemon — see
|
||||||
[Recovery paths § FD exhaustion](#fd-exhaustion-os-error-24-emfile)
|
[Recovery paths § FD exhaustion](#fd-exhaustion-os-error-24--emfile)
|
||||||
for why nothing else (prune, rebuild, `drop_index`) helps.
|
for why nothing else (prune, rebuild, `drop_index`) helps.
|
||||||
|
|
||||||
Measured cap → FD ceiling (30 add+optimize cycles + 100-query stress
|
Measured cap → FD ceiling (30 add+optimize cycles + 100-query stress
|
||||||
|
|||||||
@ -69,11 +69,13 @@ Reasons this is documented separately:
|
|||||||
│ │ ├ check-yaml / check-toml │ │
|
│ │ ├ check-yaml / check-toml │ │
|
||||||
│ │ ├ check-added-large-files (≥1MB warn) │ │
|
│ │ ├ check-added-large-files (≥1MB warn) │ │
|
||||||
│ │ ├ detect-private-key │ │
|
│ │ ├ detect-private-key │ │
|
||||||
|
│ │ ├ no committed images/videos/assets │ │
|
||||||
│ │ └ gitlint (commit-msg stage) │ │
|
│ │ └ gitlint (commit-msg stage) │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ ruff lint + format │ │
|
│ │ ruff lint + format │ │
|
||||||
│ │ (replaces black / isort / flake8) │ │
|
│ │ (replaces black / isort / flake8) │ │
|
||||||
│ │ import-linter DDD layer-direction enforcement │ │
|
│ │ import-linter DDD layer-direction enforcement │ │
|
||||||
|
│ │ repo asset gate blocks images/videos/assets in git │ │
|
||||||
│ │ pytest unit / integration │ │
|
│ │ pytest unit / integration │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └────────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
@ -94,6 +96,7 @@ Reasons this is documented separately:
|
|||||||
│ ┌─ CI/CD (GitHub Actions) ───────────────────────────────────┐ │
|
│ ┌─ CI/CD (GitHub Actions) ───────────────────────────────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ CI: .github/workflows/ci.yml lint / test / integ │ │
|
│ │ CI: .github/workflows/ci.yml lint / test / integ │ │
|
||||||
|
│ │ / package build │ │
|
||||||
│ │ Docs: .github/workflows/docs.yml Markdown + YAML check │ │
|
│ │ Docs: .github/workflows/docs.yml Markdown + YAML check │ │
|
||||||
│ │ Gates invoke Makefile targets; the Makefile is the │ │
|
│ │ Gates invoke Makefile targets; the Makefile is the │ │
|
||||||
│ │ single source of truth for commands. │ │
|
│ │ single source of truth for commands. │ │
|
||||||
@ -204,19 +207,21 @@ Stage 2: pre-commit (triggered by `git commit`)
|
|||||||
├ check-yaml, check-toml
|
├ check-yaml, check-toml
|
||||||
├ check-added-large-files (≥1MB)
|
├ check-added-large-files (≥1MB)
|
||||||
├ detect-private-key
|
├ detect-private-key
|
||||||
|
├ no-repo-assets (rejects images/videos/assets in git)
|
||||||
└ gitlint (commit-msg stage; rejects malformed messages)
|
└ gitlint (commit-msg stage; rejects malformed messages)
|
||||||
|
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Stage 3: local `make ci` (manual, before push)
|
Stage 3: local `make ci` (manual, before push)
|
||||||
├ make lint (ruff check + ruff format --check + import-linter)
|
├ make lint (ruff + import-linter + repo hygiene gates)
|
||||||
├ make test (pytest tests/unit)
|
├ make test (pytest tests/unit)
|
||||||
└ make integration (pytest tests/integration)
|
├ make integration (pytest tests/integration)
|
||||||
|
└ make package (sdist/wheel build + import smoke test)
|
||||||
|
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Stage 4: CI (GitHub Actions, push + PR triggered)
|
Stage 4: CI (GitHub Actions, push + PR triggered)
|
||||||
└ re-runs the same `make lint / test / integration` targets
|
└ re-runs the same `make lint / test / integration / package` targets
|
||||||
|
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@ -272,7 +277,7 @@ dev = ["ruff", "pytest", "pytest-asyncio", "pytest-cov",
|
|||||||
make help list all targets
|
make help list all targets
|
||||||
make install uv sync --frozen
|
make install uv sync --frozen
|
||||||
make format ruff fix + format
|
make format ruff fix + format
|
||||||
make lint ruff + import-linter + datetime discipline + openapi drift
|
make lint ruff + import-linter + repo asset/media + datetime discipline + openapi drift
|
||||||
make test pytest tests/unit
|
make test pytest tests/unit
|
||||||
make integration pytest tests/integration
|
make integration pytest tests/integration
|
||||||
make package build sdist/wheel + smoke-test wheel import
|
make package build sdist/wheel + smoke-test wheel import
|
||||||
@ -338,6 +343,7 @@ Every key has a sensible default except the `API_KEY` fields, which you fill in.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Lint | `make lint` (ruff check + ruff format --check) | any error |
|
| Lint | `make lint` (ruff check + ruff format --check) | any error |
|
||||||
| Layer direction | `make lint` (lint-imports inside) | layer violation |
|
| Layer direction | `make lint` (lint-imports inside) | layer violation |
|
||||||
|
| Repository media | `make lint` (check_repo_assets.py) | images/videos/assets committed |
|
||||||
| Datetime discipline | `make lint` (check_datetime_discipline.py) | bypasses helper module |
|
| Datetime discipline | `make lint` (check_datetime_discipline.py) | bypasses helper module |
|
||||||
| OpenAPI drift | `make lint` (dump_openapi.py --check) | schema ≠ committed openapi.json |
|
| OpenAPI drift | `make lint` (dump_openapi.py --check) | schema ≠ committed openapi.json |
|
||||||
| Unit | `make test` (pytest tests/unit) | any failure |
|
| Unit | `make test` (pytest tests/unit) | any failure |
|
||||||
|
|||||||
@ -15,6 +15,7 @@ already know what you want to do and need to know exactly how.
|
|||||||
| [cli.md](cli.md) | `everos` CLI subcommands + env var conventions |
|
| [cli.md](cli.md) | `everos` CLI subcommands + env var conventions |
|
||||||
| [storage_layout.md](storage_layout.md) | Memory-root tree + frontmatter chassis + EntryId encoding |
|
| [storage_layout.md](storage_layout.md) | Memory-root tree + frontmatter chassis + EntryId encoding |
|
||||||
| [prompt_slots.md](prompt_slots.md) | YamlConfigLoader + three-layer prompt override |
|
| [prompt_slots.md](prompt_slots.md) | YamlConfigLoader + three-layer prompt override |
|
||||||
|
| [migration-to-1.0.0.md](migration-to-1.0.0.md) | Legacy API and infrastructure migration notes for EverOS 1.0.0 |
|
||||||
|
|
||||||
## Explanation
|
## Explanation
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ Top-level project files live next to the repo root:
|
|||||||
|
|
||||||
- [README.md](../README.md) — quick start & feature overview
|
- [README.md](../README.md) — quick start & feature overview
|
||||||
- [QUICKSTART.md](../QUICKSTART.md) — 5-minute walkthrough (install → service → search)
|
- [QUICKSTART.md](../QUICKSTART.md) — 5-minute walkthrough (install → service → search)
|
||||||
|
- [use-cases.md](use-cases.md) — full use-case gallery and integration examples
|
||||||
- [CONTRIBUTING.md](../CONTRIBUTING.md) — how to contribute (issue-only model)
|
- [CONTRIBUTING.md](../CONTRIBUTING.md) — how to contribute (issue-only model)
|
||||||
- [CHANGELOG.md](../CHANGELOG.md) — release notes
|
- [CHANGELOG.md](../CHANGELOG.md) — release notes
|
||||||
- [SECURITY.md](../SECURITY.md) — security policy & private vulnerability reporting
|
- [SECURITY.md](../SECURITY.md) — security policy & private vulnerability reporting
|
||||||
|
|||||||
67
docs/migration-to-1.0.0.md
Normal file
67
docs/migration-to-1.0.0.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# EverOS 1.0.0 Migration Notes
|
||||||
|
|
||||||
|
EverOS 1.0.0 is a fresh architecture. Historical issues, examples, and
|
||||||
|
plugins may reference APIs and infrastructure that no longer exist in
|
||||||
|
the current open-source repository.
|
||||||
|
|
||||||
|
## Current 1.0.0 Contract
|
||||||
|
|
||||||
|
The supported local OSS HTTP API lives under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/v1/memory/add
|
||||||
|
POST /api/v1/memory/flush
|
||||||
|
POST /api/v1/memory/search
|
||||||
|
POST /api/v1/memory/get
|
||||||
|
```
|
||||||
|
|
||||||
|
Use [api.md](api.md) for the canonical request and response schemas.
|
||||||
|
|
||||||
|
The storage stack is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Markdown files + SQLite + LanceDB
|
||||||
|
```
|
||||||
|
|
||||||
|
MongoDB, Elasticsearch, Milvus, Redis, Kafka, longjob workers, and the
|
||||||
|
old Docker Compose stack are not part of EverOS 1.0.0.
|
||||||
|
|
||||||
|
## Legacy API Mapping
|
||||||
|
|
||||||
|
These pre-1.0.0 routes are no longer supported by the OSS repo:
|
||||||
|
|
||||||
|
| Legacy route | EverOS 1.0.0 replacement |
|
||||||
|
|---|---|
|
||||||
|
| `POST /api/v1/memories` | `POST /api/v1/memory/add` |
|
||||||
|
| `POST /api/v1/memories/group` | `POST /api/v1/memory/add` with `app_id` / `project_id` scoping |
|
||||||
|
| `GET /api/v1/memories/search` | `POST /api/v1/memory/search` |
|
||||||
|
| `POST /api/v1/memories/search` | `POST /api/v1/memory/search` |
|
||||||
|
| `GET /api/v1/memories` | `POST /api/v1/memory/get` |
|
||||||
|
| `POST /api/v1/memories/get` | `POST /api/v1/memory/get` |
|
||||||
|
| `/api/v3/agentic/*` | `POST /api/v1/memory/*` |
|
||||||
|
|
||||||
|
The 1.0.0 API also changed memory type names. For example,
|
||||||
|
`episodic_memory` is now `episode` in `/get`; `/search` returns typed
|
||||||
|
arrays such as `episodes`, `profiles`, `agent_cases`, and
|
||||||
|
`agent_skills`.
|
||||||
|
|
||||||
|
## Integration Guidance
|
||||||
|
|
||||||
|
For new integrations:
|
||||||
|
|
||||||
|
- Batch messages into one `/api/v1/memory/add` request instead of
|
||||||
|
sending one flat message object per HTTP call.
|
||||||
|
- Use `/flush` when a demo or test needs immediate extraction.
|
||||||
|
- Use `/search` for ranked recall and `/get` for paginated browsing.
|
||||||
|
- Treat old OpenClaw and EverMem Cloud plugin examples as archived
|
||||||
|
references unless they have been explicitly updated to the 1.0.0 API.
|
||||||
|
|
||||||
|
## Benchmark Guidance
|
||||||
|
|
||||||
|
The current LoCoMo reproduction path is documented in
|
||||||
|
[locomo_benchmark.md](locomo_benchmark.md). The benchmark driver uses
|
||||||
|
the 1.0.0 server API: add, flush, search, answer, and evaluate.
|
||||||
|
|
||||||
|
Old HyperMem / pre-1.0.0 evaluation pipeline reports should not be used
|
||||||
|
as 1.0.0 bug reports unless they can be reproduced with the current
|
||||||
|
benchmark commands.
|
||||||
307
docs/use-cases.md
Normal file
307
docs/use-cases.md
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
# EverOS Use Cases
|
||||||
|
|
||||||
|
Use cases show what persistent memory makes possible in real products and
|
||||||
|
workflows. Some examples are packaged in this repository; others point to
|
||||||
|
external demos or integrations you can study and adapt.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://evermind.ai/usecase_reunite)
|
||||||
|
|
||||||
|
#### Reunite - Find with EverOS
|
||||||
|
|
||||||
|
Parents describe what they remember. Children describe what they recall. Reunite uses semantic memory to surface the connections.
|
||||||
|
|
||||||
|
[Learn more](https://evermind.ai/usecase_reunite)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/tt-a1i/hive)
|
||||||
|
|
||||||
|
#### Hive Orchestrator
|
||||||
|
|
||||||
|
Browser-native hive-mind for CLI coding agents - Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.
|
||||||
|
|
||||||
|
[Code](https://github.com/tt-a1i/hive)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/tt-a1i/evermemos-mcp)
|
||||||
|
|
||||||
|
#### AI Coding Assistants with EverOS
|
||||||
|
|
||||||
|
Universal long-term memory layer for AI coding assistants, powered by EverOS.
|
||||||
|
|
||||||
|
[Code](https://github.com/tt-a1i/evermemos-mcp)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
|
#### AI Data Technician
|
||||||
|
|
||||||
|
An agentic AI system that learns from scientist interaction to inspect, analyze, and classify high-dimensional time series data - with persistent memory that improves across sessions.
|
||||||
|
|
||||||
|
[Code](https://github.com/yuansui123/AI-Data-Technician-EverMemOS)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Rokid AI Assistant with EverOS
|
||||||
|
|
||||||
|
Connect to EverOS within Rokid Glasses enabling long-term memory for all of your smart activities.
|
||||||
|
|
||||||
|
Coming soon
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Creative Assistant with Memory
|
||||||
|
|
||||||
|
Creative assistant with long-term memory, so your creative context stays available across sessions.
|
||||||
|
|
||||||
|
Coming soon
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/xunyud/Earth-Online)
|
||||||
|
|
||||||
|
#### Earth Online Memory Game
|
||||||
|
|
||||||
|
Earth Online is a memory-aware productivity game that turns everyday planning into a living quest log.
|
||||||
|
|
||||||
|
[Code](https://github.com/xunyud/Earth-Online)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
|
#### Multi-Agent Orchestration Platform
|
||||||
|
|
||||||
|
Golutra presents a multi-agent workforce for engineering teams, extending the IDE model from a single assistant to coordinated agents.
|
||||||
|
|
||||||
|
[Code](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/Yangtze-Seventh/taste-verse)
|
||||||
|
|
||||||
|
#### Your Personal Tasting Universe
|
||||||
|
|
||||||
|
Record, visualize, and explore your tasting journey through an immersive 3D star map.
|
||||||
|
|
||||||
|
[Code](https://github.com/Yangtze-Seventh/taste-verse)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
|
#### EverOS Open Her
|
||||||
|
|
||||||
|
Build AI that feels. Open-source persona engine - personality emerges from neural drives, not prompts. Inspired by Her.
|
||||||
|
|
||||||
|
[Code](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
||||||
|
|
||||||
|
#### Browser Agent for Personal Memory
|
||||||
|
|
||||||
|
Ruminer brings persistent memory to a browser agent so it can carry personal context across web tasks.
|
||||||
|
|
||||||
|
[Plugin](https://chromewebstore.google.com/detail/ruminer-browser-agent/lbccjohfpdpimbhpckljimgolndfmfif)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
|
#### EverMem Sync with EverOS
|
||||||
|
|
||||||
|
One command to connect any AI coding CLI to EverMemOS long-term memory.
|
||||||
|
|
||||||
|
[Code](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/mco-org/mco)
|
||||||
|
|
||||||
|
#### MCO - Orchestrate AI Coding Agents
|
||||||
|
|
||||||
|
MCO equips your primary agent with an agent team that can work together to solve complex tasks.
|
||||||
|
|
||||||
|
[Code](https://github.com/mco-org/mco)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
|
#### Study Buddy with Self-Evolving Memory
|
||||||
|
|
||||||
|
Study proactively with an agent that has self-evolving memory.
|
||||||
|
|
||||||
|
[Code](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/TonyLiangDesign/MemoCare)
|
||||||
|
|
||||||
|
#### Alzheimer's Memory Assistant
|
||||||
|
|
||||||
|
Empowering individuals with advanced memory support and daily assistance.
|
||||||
|
|
||||||
|
[Code](https://github.com/TonyLiangDesign/MemoCare)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
|
#### Memory-Driven Multi-Agent NPC Experience
|
||||||
|
|
||||||
|
An iOS sci-fi mystery game where players explore and uncover the truth.
|
||||||
|
|
||||||
|
[Code](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/elontusk5219-prog/Mobi)
|
||||||
|
|
||||||
|
#### Mobi Companion
|
||||||
|
|
||||||
|
An iOS app where users create, nurture, and live with a personalized AI companion called Mobi.
|
||||||
|
|
||||||
|
[Code](https://github.com/elontusk5219-prog/Mobi)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
|
#### AI Wearable with Memory
|
||||||
|
|
||||||
|
A context-native AI wearable that listens to everyday life and converts conversations into memory.
|
||||||
|
|
||||||
|
[Code](https://github.com/JaMesLiMers/EvermemCompetition-Spiro)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](migration-to-1.0.0.md)
|
||||||
|
|
||||||
|
#### Legacy OpenClaw Agent Memory
|
||||||
|
|
||||||
|
Archived pre-1.0.0 plugin reference. New integrations should use the EverOS 1.0.0 API.
|
||||||
|
|
||||||
|
[Learn more](migration-to-1.0.0.md)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
||||||
|
|
||||||
|
#### Live2D Character with Memory
|
||||||
|
|
||||||
|
Add long-term memory to a real-time Live2D character, powered by [TEN Framework](https://github.com/TEN-framework/ten-framework).
|
||||||
|
|
||||||
|
[Code](https://github.com/TEN-framework/ten-framework/tree/04cb80601374fa9e35b4e544b2dbd23286ca7763/ai_agents/agents/examples/voice-assistant-with-EverMemOS)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://screenshot-analysis-vercel.vercel.app/)
|
||||||
|
|
||||||
|
#### Computer-Use with Memory
|
||||||
|
|
||||||
|
Run screenshot-based analysis with computer-use and store the results in memory.
|
||||||
|
|
||||||
|
[Live Demo](https://screenshot-analysis-vercel.vercel.app/)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](../use-cases/game-of-throne-demo)
|
||||||
|
|
||||||
|
#### Game of Thrones Memories
|
||||||
|
|
||||||
|
A demonstration of AI memory infrastructure through an interactive Q&A experience with *A Game of Thrones*.
|
||||||
|
|
||||||
|
[Code](../use-cases/game-of-throne-demo)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](../use-cases/claude-code-plugin)
|
||||||
|
|
||||||
|
#### Claude Code Plugin
|
||||||
|
|
||||||
|
Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.
|
||||||
|
|
||||||
|
[Code](../use-cases/claude-code-plugin)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
[](https://main.d2j21qxnymu6wl.amplifyapp.com/graph.html)
|
||||||
|
|
||||||
|
#### Memory Graph Visualization
|
||||||
|
|
||||||
|
Explore stored entities and relationships in a graph interface. Frontend demo; backend integration is in progress.
|
||||||
|
|
||||||
|
[Live Demo](https://main.d2j21qxnymu6wl.amplifyapp.com/graph.html)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[Back to README](../README.md)
|
||||||
@ -21,9 +21,7 @@ ALLOWED_TYPES = (
|
|||||||
"ci",
|
"ci",
|
||||||
"revert",
|
"revert",
|
||||||
)
|
)
|
||||||
TITLE_RE = re.compile(
|
TITLE_RE = re.compile(rf"^({'|'.join(ALLOWED_TYPES)})(\([A-Za-z0-9._/-]+\))?(!)?: .+")
|
||||||
rf"^({'|'.join(ALLOWED_TYPES)})(\([A-Za-z0-9._/-]+\))?(!)?: .+"
|
|
||||||
)
|
|
||||||
MAX_TITLE_LENGTH = 72
|
MAX_TITLE_LENGTH = 72
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +31,9 @@ def _run_git(args: list[str]) -> str:
|
|||||||
|
|
||||||
def _default_range() -> str:
|
def _default_range() -> str:
|
||||||
event_name = os.getenv("GITHUB_EVENT_NAME", "")
|
event_name = os.getenv("GITHUB_EVENT_NAME", "")
|
||||||
before = os.getenv("GITHUB_EVENT_BEFORE", "") or os.getenv("GITHUB_EVENT_BEFORE_SHA", "")
|
before = os.getenv("GITHUB_EVENT_BEFORE", "") or os.getenv(
|
||||||
|
"GITHUB_EVENT_BEFORE_SHA", ""
|
||||||
|
)
|
||||||
after = os.getenv("GITHUB_SHA", "HEAD")
|
after = os.getenv("GITHUB_SHA", "HEAD")
|
||||||
pr_base = os.getenv("GITHUB_PR_BASE_SHA", "")
|
pr_base = os.getenv("GITHUB_PR_BASE_SHA", "")
|
||||||
|
|
||||||
@ -83,7 +83,8 @@ def _validate(commit_range: str) -> list[str]:
|
|||||||
short = commit[:12]
|
short = commit[:12]
|
||||||
if len(subject) > MAX_TITLE_LENGTH:
|
if len(subject) > MAX_TITLE_LENGTH:
|
||||||
failures.append(
|
failures.append(
|
||||||
f"{short}: subject is {len(subject)} chars; max is {MAX_TITLE_LENGTH}: {subject}"
|
f"{short}: subject is {len(subject)} chars; "
|
||||||
|
f"max is {MAX_TITLE_LENGTH}: {subject}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
86
scripts/check_deprecated_names.py
Normal file
86
scripts/check_deprecated_names.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""Block deprecated product names in tracked repository text."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEPRECATED_NAME_RE = re.compile(r"\bever[\s_-]*core\b", flags=re.IGNORECASE)
|
||||||
|
SKIP_SUFFIXES = frozenset(
|
||||||
|
{
|
||||||
|
".avif",
|
||||||
|
".bmp",
|
||||||
|
".gif",
|
||||||
|
".heic",
|
||||||
|
".heif",
|
||||||
|
".icns",
|
||||||
|
".ico",
|
||||||
|
".jpeg",
|
||||||
|
".jpg",
|
||||||
|
".mov",
|
||||||
|
".mp4",
|
||||||
|
".png",
|
||||||
|
".webp",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Violation:
|
||||||
|
path: str
|
||||||
|
line_number: int
|
||||||
|
line: str
|
||||||
|
|
||||||
|
|
||||||
|
def find_violations(files: Iterable[tuple[str, str]]) -> list[Violation]:
|
||||||
|
violations: list[Violation] = []
|
||||||
|
for path, text in files:
|
||||||
|
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||||
|
if DEPRECATED_NAME_RE.search(line):
|
||||||
|
violations.append(
|
||||||
|
Violation(path=path, line_number=line_number, line=line.strip())
|
||||||
|
)
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _tracked_paths() -> list[Path]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "ls-files", "-z"],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=False,
|
||||||
|
)
|
||||||
|
return [Path(raw.decode("utf-8")) for raw in result.stdout.split(b"\0") if raw]
|
||||||
|
|
||||||
|
|
||||||
|
def _tracked_text_files() -> Iterable[tuple[str, str]]:
|
||||||
|
for path in _tracked_paths():
|
||||||
|
if path.suffix.lower() in SKIP_SUFFIXES:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
yield path.as_posix(), text
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
violations = find_violations(_tracked_text_files())
|
||||||
|
if not violations:
|
||||||
|
print("Deprecated-name check passed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Deprecated-name check failed.\n"
|
||||||
|
"Use EverOS or EverMind Cloud. Do not use deprecated product naming.\n"
|
||||||
|
)
|
||||||
|
for violation in violations:
|
||||||
|
print(f"- {violation.path}:{violation.line_number}: {violation.line}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SKIP_DIRS = {".git", "node_modules", ".venv", ".uv-cache"}
|
SKIP_DIRS = {".git", "node_modules", ".venv", ".uv-cache"}
|
||||||
|
|||||||
70
scripts/check_pr_title.py
Normal file
70
scripts/check_pr_title.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""Validate pull request titles against the EverOS Conventional Commits policy."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _load_commit_policy() -> ModuleType:
|
||||||
|
policy_path = _SCRIPT_DIR / "check_commit_messages.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("_commit_message_policy", policy_path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Unable to load commit policy from {policy_path}")
|
||||||
|
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
_POLICY = _load_commit_policy()
|
||||||
|
ALLOWED_TYPES = _POLICY.ALLOWED_TYPES
|
||||||
|
MAX_TITLE_LENGTH = _POLICY.MAX_TITLE_LENGTH
|
||||||
|
TITLE_RE = _POLICY.TITLE_RE
|
||||||
|
|
||||||
|
|
||||||
|
def validate_title(title: str) -> list[str]:
|
||||||
|
title = title.strip()
|
||||||
|
if not title:
|
||||||
|
return ["missing PR title"]
|
||||||
|
|
||||||
|
if len(title) > MAX_TITLE_LENGTH:
|
||||||
|
return [f"PR title is {len(title)} chars; max is {MAX_TITLE_LENGTH}: {title}"]
|
||||||
|
|
||||||
|
if not TITLE_RE.match(title):
|
||||||
|
allowed = ", ".join(ALLOWED_TYPES)
|
||||||
|
return [
|
||||||
|
f"invalid PR title: {title}\n"
|
||||||
|
" expected: <type>[(scope)][!]: <description>\n"
|
||||||
|
f" allowed types: {allowed}"
|
||||||
|
]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _title_from_args_or_env(argv: list[str]) -> str:
|
||||||
|
if argv:
|
||||||
|
return " ".join(argv)
|
||||||
|
return os.getenv("PR_TITLE", "")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
title = _title_from_args_or_env(sys.argv[1:] if argv is None else argv)
|
||||||
|
failures = validate_title(title)
|
||||||
|
if failures:
|
||||||
|
print("Pull request title check failed:")
|
||||||
|
print("\n".join(failures))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("Pull request title follows Conventional Commits.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
117
scripts/check_repo_assets.py
Normal file
117
scripts/check_repo_assets.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""Block committed image/video files and asset-style directories."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
|
||||||
|
BLOCKED_DIR_NAMES = frozenset(
|
||||||
|
{
|
||||||
|
"asset",
|
||||||
|
"assets",
|
||||||
|
"image",
|
||||||
|
"images",
|
||||||
|
"img",
|
||||||
|
"media",
|
||||||
|
"video",
|
||||||
|
"videos",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
IMAGE_EXTENSIONS = frozenset(
|
||||||
|
{
|
||||||
|
".avif",
|
||||||
|
".bmp",
|
||||||
|
".gif",
|
||||||
|
".heic",
|
||||||
|
".heif",
|
||||||
|
".icns",
|
||||||
|
".ico",
|
||||||
|
".jpeg",
|
||||||
|
".jpg",
|
||||||
|
".png",
|
||||||
|
".svg",
|
||||||
|
".tif",
|
||||||
|
".tiff",
|
||||||
|
".webp",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
VIDEO_EXTENSIONS = frozenset(
|
||||||
|
{
|
||||||
|
".avi",
|
||||||
|
".flv",
|
||||||
|
".m4v",
|
||||||
|
".mkv",
|
||||||
|
".mov",
|
||||||
|
".mp4",
|
||||||
|
".mpeg",
|
||||||
|
".mpg",
|
||||||
|
".webm",
|
||||||
|
".wmv",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Violation:
|
||||||
|
path: str
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_path(path: str) -> PurePosixPath:
|
||||||
|
return PurePosixPath(path.replace("\\", "/"))
|
||||||
|
|
||||||
|
|
||||||
|
def _violation_reason(path: str) -> str | None:
|
||||||
|
posix_path = _normalise_path(path)
|
||||||
|
lower_parts = tuple(part.lower() for part in posix_path.parts)
|
||||||
|
if any(part in BLOCKED_DIR_NAMES for part in lower_parts):
|
||||||
|
return "asset/media directory"
|
||||||
|
|
||||||
|
suffix = posix_path.suffix.lower()
|
||||||
|
if suffix in IMAGE_EXTENSIONS:
|
||||||
|
return "image file"
|
||||||
|
if suffix in VIDEO_EXTENSIONS:
|
||||||
|
return "video file"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_violations(paths: Iterable[str]) -> list[Violation]:
|
||||||
|
violations: list[Violation] = []
|
||||||
|
for path in paths:
|
||||||
|
reason = _violation_reason(path)
|
||||||
|
if reason is not None:
|
||||||
|
violations.append(Violation(path=path, reason=reason))
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _tracked_paths() -> list[str]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "ls-files", "-z"],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=False,
|
||||||
|
)
|
||||||
|
return [raw.decode("utf-8") for raw in result.stdout.split(b"\0") if raw]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
violations = find_violations(_tracked_paths())
|
||||||
|
if not violations:
|
||||||
|
print("Repository asset/media check passed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Repository asset/media check failed.\n"
|
||||||
|
"Do not commit images, videos, or asset/media directories. "
|
||||||
|
"Host visual media externally, in release artifacts, or another "
|
||||||
|
"approved storage location, then link to it from docs.\n"
|
||||||
|
)
|
||||||
|
for violation in violations:
|
||||||
|
print(f"- {violation.path}: {violation.reason}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -9,9 +9,16 @@ provider) instead of silently failing per-request downstream.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from everalgo.llm import build_client
|
from everalgo.llm import build_client
|
||||||
from everalgo.llm.config import LLMConfig
|
from everalgo.llm.config import LLMConfig
|
||||||
from everalgo.llm.protocols import LLMClient
|
from everalgo.llm.protocols import LLMClient
|
||||||
|
from everalgo.llm.types import ChatMessage, ChatResponse, ImageUrlPart, TextPart
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from everos.config import load_settings
|
from everos.config import load_settings
|
||||||
from everos.core.observability.logging import get_logger
|
from everos.core.observability.logging import get_logger
|
||||||
@ -25,6 +32,212 @@ class LLMNotConfiguredError(RuntimeError):
|
|||||||
|
|
||||||
_llm_client: LLMClient | None = None
|
_llm_client: LLMClient | None = None
|
||||||
_multimodal_client: LLMClient | None = None
|
_multimodal_client: LLMClient | None = None
|
||||||
|
_VLM_IMAGE_MIN_SIDE = 1024
|
||||||
|
_NO_THINKING_EXTRA_BODY_KEY = "chat_template_kwargs"
|
||||||
|
_NO_THINKING_PARAM = {"enable_thinking": False}
|
||||||
|
|
||||||
|
_IMAGE_VISUAL_MEMORY_PROMPT = """Describe this image for visual memory retrieval.
|
||||||
|
|
||||||
|
Output final Markdown directly; do not include reasoning.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Key visible objects and their names, brands, colors, labels, quantities.
|
||||||
|
2. Spatial relationships and relative positions: left/right/above/below/center,
|
||||||
|
foreground/background, nearby objects, and supporting surfaces.
|
||||||
|
3. Location-query facts, e.g. "the milk carton is center-left, to the right of
|
||||||
|
X and to the left of Y".
|
||||||
|
4. Important visible text, but extract only useful labels/interface text; do
|
||||||
|
not exhaustively OCR every key or menu item if that would crowd out object
|
||||||
|
locations.
|
||||||
|
|
||||||
|
Do NOT describe the parser, assistant, or ChatGPT as processing the image.
|
||||||
|
If "ChatGPT" is visible, list it only as visible interface text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class _NoThinkingRequestDefaultsClient:
|
||||||
|
"""Inject default no-thinking request params for OpenAI-compatible servers."""
|
||||||
|
|
||||||
|
def __init__(self, inner: LLMClient) -> None:
|
||||||
|
self._inner = inner
|
||||||
|
|
||||||
|
async def chat(
|
||||||
|
self,
|
||||||
|
messages: list[ChatMessage],
|
||||||
|
*,
|
||||||
|
model: str | None = None,
|
||||||
|
temperature: float | None = None,
|
||||||
|
max_tokens: int | None = None,
|
||||||
|
response_format: type[BaseModel] | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> ChatResponse:
|
||||||
|
return await self._inner.chat(
|
||||||
|
messages,
|
||||||
|
model=model,
|
||||||
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
response_format=response_format,
|
||||||
|
**_with_no_thinking_defaults(extra),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _MultimodalImageDetailCompatClient:
|
||||||
|
"""Patch image parts for strict OpenAI-compatible gateways.
|
||||||
|
|
||||||
|
everalgo-core 0.2.0 serialises ``image_url.detail`` as ``None`` when the
|
||||||
|
field is unset. Some gateways reject that literal null and require one of
|
||||||
|
OpenAI's enum values. EverOS only uses this wrapper for multimodal parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, inner: LLMClient, *, resize_images_for_vlm: bool) -> None:
|
||||||
|
self._inner = inner
|
||||||
|
self._resize_images_for_vlm = resize_images_for_vlm
|
||||||
|
|
||||||
|
async def chat(
|
||||||
|
self,
|
||||||
|
messages: list[ChatMessage],
|
||||||
|
*,
|
||||||
|
model: str | None = None,
|
||||||
|
temperature: float | None = None,
|
||||||
|
max_tokens: int | None = None,
|
||||||
|
response_format: type[BaseModel] | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> ChatResponse:
|
||||||
|
return await self._inner.chat(
|
||||||
|
[
|
||||||
|
_with_multimodal_image_defaults(
|
||||||
|
m,
|
||||||
|
resize_images_for_vlm=self._resize_images_for_vlm,
|
||||||
|
)
|
||||||
|
for m in messages
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
response_format=response_format,
|
||||||
|
**_with_no_thinking_defaults(extra),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _with_no_thinking_defaults(extra: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Return request kwargs with no-thinking enabled unless caller overrides."""
|
||||||
|
patched = dict(extra)
|
||||||
|
extra_body = dict(patched.get("extra_body") or {})
|
||||||
|
chat_template_kwargs = dict(extra_body.get(_NO_THINKING_EXTRA_BODY_KEY) or {})
|
||||||
|
chat_template_kwargs.setdefault(
|
||||||
|
"enable_thinking", _NO_THINKING_PARAM["enable_thinking"]
|
||||||
|
)
|
||||||
|
extra_body[_NO_THINKING_EXTRA_BODY_KEY] = chat_template_kwargs
|
||||||
|
patched["extra_body"] = extra_body
|
||||||
|
return patched
|
||||||
|
|
||||||
|
|
||||||
|
def _with_multimodal_image_defaults(
|
||||||
|
message: ChatMessage, *, resize_images_for_vlm: bool = True
|
||||||
|
) -> ChatMessage:
|
||||||
|
"""Return a copy with stricter-gateway + visual-memory image defaults."""
|
||||||
|
content = message.content
|
||||||
|
if not isinstance(content, list):
|
||||||
|
return message
|
||||||
|
|
||||||
|
has_image = any(_is_image_part(part) for part in content)
|
||||||
|
instructions_added = False
|
||||||
|
changed = False
|
||||||
|
patched_parts: list[object] = []
|
||||||
|
for part in content:
|
||||||
|
patched = part
|
||||||
|
if isinstance(part, ImageUrlPart):
|
||||||
|
image_url_updates: dict[str, object] = {}
|
||||||
|
if part.image_url.detail is None:
|
||||||
|
image_url_updates["detail"] = "auto"
|
||||||
|
if resize_images_for_vlm:
|
||||||
|
resized_url = _resize_image_data_url(part.image_url.url)
|
||||||
|
if resized_url != part.image_url.url:
|
||||||
|
image_url_updates["url"] = resized_url
|
||||||
|
if image_url_updates:
|
||||||
|
image_url = part.image_url.model_copy(update=image_url_updates)
|
||||||
|
patched = part.model_copy(update={"image_url": image_url})
|
||||||
|
changed = True
|
||||||
|
if (
|
||||||
|
has_image
|
||||||
|
and not instructions_added
|
||||||
|
and isinstance(patched, TextPart)
|
||||||
|
and patched.text != _IMAGE_VISUAL_MEMORY_PROMPT
|
||||||
|
):
|
||||||
|
patched = patched.model_copy(
|
||||||
|
update={"text": _IMAGE_VISUAL_MEMORY_PROMPT}
|
||||||
|
)
|
||||||
|
instructions_added = True
|
||||||
|
changed = True
|
||||||
|
patched_parts.append(patched)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return message
|
||||||
|
return message.model_copy(update={"content": patched_parts})
|
||||||
|
|
||||||
|
|
||||||
|
def _is_image_part(part: object) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(part, ImageUrlPart)
|
||||||
|
and part.image_url.url.startswith("data:image/")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resize_image_data_url(url: str) -> str:
|
||||||
|
"""Resize base64 data-url images so the shorter side is 64 pixels."""
|
||||||
|
if not url.startswith("data:image/"):
|
||||||
|
return url
|
||||||
|
try:
|
||||||
|
header, encoded = url.split(",", 1)
|
||||||
|
except ValueError:
|
||||||
|
return url
|
||||||
|
if ";base64" not in header.lower():
|
||||||
|
return url
|
||||||
|
|
||||||
|
mime_type = header[5:].split(";", 1)[0].lower()
|
||||||
|
image_format = {
|
||||||
|
"image/jpeg": "JPEG",
|
||||||
|
"image/jpg": "JPEG",
|
||||||
|
"image/png": "PNG",
|
||||||
|
"image/webp": "WEBP",
|
||||||
|
}.get(mime_type)
|
||||||
|
if image_format is None:
|
||||||
|
return url
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
raw = base64.b64decode(encoded, validate=True)
|
||||||
|
with Image.open(BytesIO(raw)) as image:
|
||||||
|
image = ImageOps.exif_transpose(image)
|
||||||
|
target_size = _image_size_with_min_side(
|
||||||
|
image.size, _VLM_IMAGE_MIN_SIDE
|
||||||
|
)
|
||||||
|
resized = image.resize(target_size, Image.Resampling.LANCZOS)
|
||||||
|
if image_format == "JPEG" and resized.mode not in ("RGB", "L"):
|
||||||
|
resized = resized.convert("RGB")
|
||||||
|
buffer = BytesIO()
|
||||||
|
save_kwargs: dict[str, object] = {"format": image_format}
|
||||||
|
if image_format == "JPEG":
|
||||||
|
save_kwargs["quality"] = 85
|
||||||
|
resized.save(buffer, **save_kwargs)
|
||||||
|
except (ImportError, ValueError, OSError, binascii.Error):
|
||||||
|
return url
|
||||||
|
|
||||||
|
resized_encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
|
||||||
|
return f"{header},{resized_encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
def _image_size_with_min_side(
|
||||||
|
size: tuple[int, int],
|
||||||
|
min_side: int,
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
width, height = size
|
||||||
|
shortest = min(width, height)
|
||||||
|
if shortest <= 0:
|
||||||
|
return (max(1, width), max(1, height))
|
||||||
|
scale = min_side / shortest
|
||||||
|
return (max(1, round(width * scale)), max(1, round(height * scale)))
|
||||||
|
|
||||||
|
|
||||||
def get_llm_client() -> LLMClient:
|
def get_llm_client() -> LLMClient:
|
||||||
@ -46,11 +259,14 @@ def get_llm_client() -> LLMClient:
|
|||||||
raise LLMNotConfiguredError(
|
raise LLMNotConfiguredError(
|
||||||
"LLM is required; set EVEROS_LLM__API_KEY + EVEROS_LLM__BASE_URL"
|
"LLM is required; set EVEROS_LLM__API_KEY + EVEROS_LLM__BASE_URL"
|
||||||
)
|
)
|
||||||
_llm_client = build_client(
|
_llm_client = _NoThinkingRequestDefaultsClient(
|
||||||
LLMConfig(
|
build_client(
|
||||||
model=llm_cfg.model,
|
LLMConfig(
|
||||||
api_key=api_key,
|
model=llm_cfg.model,
|
||||||
base_url=llm_cfg.base_url,
|
api_key=api_key,
|
||||||
|
base_url=llm_cfg.base_url,
|
||||||
|
timeout=llm_cfg.timeout_seconds,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.info("llm_client_built", model=llm_cfg.model)
|
logger.info("llm_client_built", model=llm_cfg.model)
|
||||||
@ -78,12 +294,16 @@ def get_multimodal_llm_client() -> LLMClient:
|
|||||||
"Multimodal LLM is required for parsing; set "
|
"Multimodal LLM is required for parsing; set "
|
||||||
"EVEROS_MULTIMODAL__API_KEY + EVEROS_MULTIMODAL__BASE_URL"
|
"EVEROS_MULTIMODAL__API_KEY + EVEROS_MULTIMODAL__BASE_URL"
|
||||||
)
|
)
|
||||||
_multimodal_client = build_client(
|
_multimodal_client = _MultimodalImageDetailCompatClient(
|
||||||
LLMConfig(
|
build_client(
|
||||||
model=cfg.model,
|
LLMConfig(
|
||||||
api_key=api_key,
|
model=cfg.model,
|
||||||
base_url=cfg.base_url,
|
api_key=api_key,
|
||||||
)
|
base_url=cfg.base_url,
|
||||||
|
timeout=cfg.timeout_seconds,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
resize_images_for_vlm=cfg.resize_images_for_vlm,
|
||||||
)
|
)
|
||||||
logger.info("multimodal_llm_client_built", model=cfg.model)
|
logger.info("multimodal_llm_client_built", model=cfg.model)
|
||||||
return _multimodal_client
|
return _multimodal_client
|
||||||
|
|||||||
@ -42,4 +42,5 @@ def build_llm_provider(settings: LLMSettings) -> LLMClient:
|
|||||||
model=settings.model,
|
model=settings.model,
|
||||||
api_key=settings.api_key.get_secret_value(),
|
api_key=settings.api_key.get_secret_value(),
|
||||||
base_url=settings.base_url,
|
base_url=settings.base_url,
|
||||||
|
timeout=settings.timeout_seconds,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -39,7 +39,7 @@ from .protocol import RerankError, RerankResult
|
|||||||
|
|
||||||
# Qwen3-Reranker chat template. The DeepInfra inference API treats the reranker
|
# Qwen3-Reranker chat template. The DeepInfra inference API treats the reranker
|
||||||
# as a yes/no generator, so the prompt scaffolding must be supplied client-side
|
# as a yes/no generator, so the prompt scaffolding must be supplied client-side
|
||||||
# (verbatim mirror of the EverCore benchmark's reranker client). Without it the
|
# (verbatim mirror of the benchmark reranker client). Without it the
|
||||||
# model scores raw text off-template and returns uncalibrated relevance.
|
# model scores raw text off-template and returns uncalibrated relevance.
|
||||||
_QWEN3_PREFIX = (
|
_QWEN3_PREFIX = (
|
||||||
"<|im_start|>system\n"
|
"<|im_start|>system\n"
|
||||||
|
|||||||
@ -56,17 +56,21 @@ cache_size_kb = 2048
|
|||||||
|
|
||||||
[llm]
|
[llm]
|
||||||
# Provider-agnostic OpenAI-protocol client config. Override via env:
|
# Provider-agnostic OpenAI-protocol client config. Override via env:
|
||||||
# EVEROS_LLM__MODEL, EVEROS_LLM__API_KEY, EVEROS_LLM__BASE_URL
|
# EVEROS_LLM__MODEL, EVEROS_LLM__API_KEY, EVEROS_LLM__BASE_URL, EVEROS_LLM__TIMEOUT_SECONDS
|
||||||
# Or via a ``.env`` file next to the project root (auto-loaded).
|
# Or via a ``.env`` file next to the project root (auto-loaded).
|
||||||
model = "gpt-4o-mini"
|
model = "gpt-4o-mini"
|
||||||
|
timeout_seconds = 180.0
|
||||||
# api_key = ""
|
# api_key = ""
|
||||||
# base_url = ""
|
# base_url = ""
|
||||||
|
|
||||||
[multimodal]
|
[multimodal]
|
||||||
# Independent LLM for multimodal parsing (everalgo-parser); must accept
|
# Independent LLM for multimodal parsing (everalgo-parser); must accept
|
||||||
# image / pdf / audio image_url parts. Override via env:
|
# image / pdf / audio image_url parts. Override via env:
|
||||||
# EVEROS_MULTIMODAL__MODEL, EVEROS_MULTIMODAL__API_KEY, EVEROS_MULTIMODAL__BASE_URL
|
# EVEROS_MULTIMODAL__MODEL, EVEROS_MULTIMODAL__API_KEY, EVEROS_MULTIMODAL__BASE_URL,
|
||||||
|
# EVEROS_MULTIMODAL__TIMEOUT_SECONDS, EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM
|
||||||
model = "google/gemini-3-flash-preview"
|
model = "google/gemini-3-flash-preview"
|
||||||
|
timeout_seconds = 180.0
|
||||||
|
resize_images_for_vlm = true
|
||||||
max_concurrency = 4
|
max_concurrency = 4
|
||||||
# api_key = ""
|
# api_key = ""
|
||||||
# base_url = ""
|
# base_url = ""
|
||||||
|
|||||||
@ -121,11 +121,13 @@ class LLMSettings(BaseModel):
|
|||||||
EVEROS_LLM__MODEL
|
EVEROS_LLM__MODEL
|
||||||
EVEROS_LLM__API_KEY
|
EVEROS_LLM__API_KEY
|
||||||
EVEROS_LLM__BASE_URL
|
EVEROS_LLM__BASE_URL
|
||||||
|
EVEROS_LLM__TIMEOUT_SECONDS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model: str = "gpt-4o-mini"
|
model: str = "gpt-4o-mini"
|
||||||
api_key: SecretStr | None = None
|
api_key: SecretStr | None = None
|
||||||
base_url: str | None = None
|
base_url: str | None = None
|
||||||
|
timeout_seconds: float = Field(default=180.0, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class MultimodalSettings(BaseModel):
|
class MultimodalSettings(BaseModel):
|
||||||
@ -140,6 +142,8 @@ class MultimodalSettings(BaseModel):
|
|||||||
EVEROS_MULTIMODAL__MODEL
|
EVEROS_MULTIMODAL__MODEL
|
||||||
EVEROS_MULTIMODAL__API_KEY
|
EVEROS_MULTIMODAL__API_KEY
|
||||||
EVEROS_MULTIMODAL__BASE_URL
|
EVEROS_MULTIMODAL__BASE_URL
|
||||||
|
EVEROS_MULTIMODAL__TIMEOUT_SECONDS
|
||||||
|
EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM
|
||||||
EVEROS_MULTIMODAL__MAX_CONCURRENCY
|
EVEROS_MULTIMODAL__MAX_CONCURRENCY
|
||||||
EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS
|
EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS
|
||||||
EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES
|
EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES
|
||||||
@ -148,6 +152,8 @@ class MultimodalSettings(BaseModel):
|
|||||||
model: str = "google/gemini-3-flash-preview"
|
model: str = "google/gemini-3-flash-preview"
|
||||||
api_key: SecretStr | None = None
|
api_key: SecretStr | None = None
|
||||||
base_url: str | None = None
|
base_url: str | None = None
|
||||||
|
timeout_seconds: float = Field(default=180.0, gt=0)
|
||||||
|
resize_images_for_vlm: bool = True
|
||||||
max_concurrency: int = 4
|
max_concurrency: int = 4
|
||||||
|
|
||||||
# ``file://`` content-item support (read locally by EverOS, not everalgo).
|
# ``file://`` content-item support (read locally by EverOS, not everalgo).
|
||||||
|
|||||||
@ -180,4 +180,6 @@ def register(parent: typer.Typer) -> None:
|
|||||||
typer.echo("Next steps:")
|
typer.echo("Next steps:")
|
||||||
typer.echo(" 1. Edit the file and fill in the API keys (see comments inside).")
|
typer.echo(" 1. Edit the file and fill in the API keys (see comments inside).")
|
||||||
typer.echo(" 2. Run `everos server start`.")
|
typer.echo(" 2. Run `everos server start`.")
|
||||||
typer.echo("Docs: https://github.com/evermind/everos/blob/master/QUICKSTART.md")
|
typer.echo(
|
||||||
|
"Docs: https://github.com/EverMind-AI/EverOS/blob/main/QUICKSTART.md"
|
||||||
|
)
|
||||||
|
|||||||
@ -47,6 +47,7 @@ class CascadeWatcher:
|
|||||||
self._observer = Observer()
|
self._observer = Observer()
|
||||||
self._handler = _Handler(memory_root, loop)
|
self._handler = _Handler(memory_root, loop)
|
||||||
self._started = False
|
self._started = False
|
||||||
|
self._observer_started = False
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
if self._started:
|
if self._started:
|
||||||
@ -54,18 +55,31 @@ class CascadeWatcher:
|
|||||||
# The memory root is created lazily by other layers; watchdog
|
# The memory root is created lazily by other layers; watchdog
|
||||||
# rejects non-existent paths so we ensure it exists here.
|
# rejects non-existent paths so we ensure it exists here.
|
||||||
self._memory_root.ensure()
|
self._memory_root.ensure()
|
||||||
self._observer.schedule(
|
watch_roots = _watch_roots(self._memory_root.root)
|
||||||
self._handler, str(self._memory_root.root), recursive=True
|
for root in watch_roots:
|
||||||
)
|
self._observer.schedule(self._handler, str(root), recursive=True)
|
||||||
self._observer.start()
|
if watch_roots:
|
||||||
|
self._observer.start()
|
||||||
|
self._observer_started = True
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"cascade_watcher_no_user_visible_roots",
|
||||||
|
root=str(self._memory_root.root),
|
||||||
|
)
|
||||||
self._started = True
|
self._started = True
|
||||||
logger.info("cascade_watcher_started", root=str(self._memory_root.root))
|
logger.info(
|
||||||
|
"cascade_watcher_started",
|
||||||
|
root=str(self._memory_root.root),
|
||||||
|
watched_roots=[str(root) for root in watch_roots],
|
||||||
|
)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
if not self._started:
|
if not self._started:
|
||||||
return
|
return
|
||||||
self._observer.stop()
|
if self._observer_started:
|
||||||
self._observer.join(timeout=5)
|
self._observer.stop()
|
||||||
|
self._observer.join(timeout=5)
|
||||||
|
self._observer_started = False
|
||||||
self._started = False
|
self._started = False
|
||||||
logger.info("cascade_watcher_stopped")
|
logger.info("cascade_watcher_stopped")
|
||||||
|
|
||||||
@ -163,6 +177,22 @@ def _relative_to_root(root: Path, raw: str) -> str | None:
|
|||||||
return rel.as_posix()
|
return rel.as_posix()
|
||||||
|
|
||||||
|
|
||||||
|
def _watch_roots(root: Path) -> list[Path]:
|
||||||
|
"""Return user-visible top-level dirs to watch, excluding system dot dirs."""
|
||||||
|
try:
|
||||||
|
children = list(root.iterdir())
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return sorted(
|
||||||
|
(
|
||||||
|
child
|
||||||
|
for child in children
|
||||||
|
if child.is_dir() and not child.name.startswith(".")
|
||||||
|
),
|
||||||
|
key=lambda p: p.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _safe_mtime(raw: str) -> float:
|
def _safe_mtime(raw: str) -> float:
|
||||||
"""Return mtime in seconds, falling back to 0.0 on stat failure."""
|
"""Return mtime in seconds, falling back to 0.0 on stat failure."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -17,6 +17,11 @@ from everos.core.observability.logging import get_logger
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
_IMAGE_VISUAL_FACTS_NOTE = (
|
||||||
|
"Context: image visual facts extracted from an uploaded image; "
|
||||||
|
"treat these as image content, not assistant actions."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def coerce_items(
|
def coerce_items(
|
||||||
content: str | list[dict[str, Any]] | list[Any],
|
content: str | list[dict[str, Any]] | list[Any],
|
||||||
@ -83,6 +88,8 @@ def _render_item(item: dict[str, Any]) -> str | None:
|
|||||||
kind = str(item.get("type") or "file").upper()
|
kind = str(item.get("type") or "file").upper()
|
||||||
name = item.get("name") or ""
|
name = item.get("name") or ""
|
||||||
tag = f"[{kind}: {name}]" if name else f"[{kind}]"
|
tag = f"[{kind}: {name}]" if name else f"[{kind}]"
|
||||||
|
if kind == "IMAGE":
|
||||||
|
return f"{tag}\n{_IMAGE_VISUAL_FACTS_NOTE}\n{parsed}"
|
||||||
return f"{tag}\n{parsed}"
|
return f"{tag}\n{parsed}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -518,7 +518,7 @@ class SearchManager:
|
|||||||
``atomic_fact`` table) for finer-grained semantic match — long
|
``atomic_fact`` table) for finer-grained semantic match — long
|
||||||
episodes whose single mean-pooled vector dilutes a specific topic
|
episodes whose single mean-pooled vector dilutes a specific topic
|
||||||
recover via the matching atomic fact's own embedding. Mirrors
|
recover via the matching atomic fact's own embedding. Mirrors
|
||||||
EverOS/EverCore's MaxSim retrieval pattern.
|
the EverOS MaxSim retrieval pattern.
|
||||||
"""
|
"""
|
||||||
vector = await self._embed_query(req.query)
|
vector = await self._embed_query(req.query)
|
||||||
if not vector:
|
if not vector:
|
||||||
|
|||||||
@ -33,6 +33,8 @@
|
|||||||
EVEROS_LLM__MODEL=openai/gpt-4.1-mini
|
EVEROS_LLM__MODEL=openai/gpt-4.1-mini
|
||||||
EVEROS_LLM__API_KEY=
|
EVEROS_LLM__API_KEY=
|
||||||
EVEROS_LLM__BASE_URL=https://openrouter.ai/api/v1
|
EVEROS_LLM__BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
# Per-request chat-completions timeout in seconds (default 180):
|
||||||
|
# EVEROS_LLM__TIMEOUT_SECONDS=180
|
||||||
|
|
||||||
|
|
||||||
# ─── Multimodal LLM (independent from [llm]; vision/audio capable) ────
|
# ─── Multimodal LLM (independent from [llm]; vision/audio capable) ────
|
||||||
@ -43,6 +45,11 @@ EVEROS_LLM__BASE_URL=https://openrouter.ai/api/v1
|
|||||||
EVEROS_MULTIMODAL__MODEL=google/gemini-3-flash-preview
|
EVEROS_MULTIMODAL__MODEL=google/gemini-3-flash-preview
|
||||||
EVEROS_MULTIMODAL__API_KEY=
|
EVEROS_MULTIMODAL__API_KEY=
|
||||||
EVEROS_MULTIMODAL__BASE_URL=https://openrouter.ai/api/v1
|
EVEROS_MULTIMODAL__BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
# Per-request multimodal chat-completions timeout in seconds (default 180):
|
||||||
|
# EVEROS_MULTIMODAL__TIMEOUT_SECONDS=180
|
||||||
|
# Resize inline images to half width/height before sending them to the VLM
|
||||||
|
# (default true):
|
||||||
|
# EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM=true
|
||||||
# Concurrency cap for parallel multimodal calls (default 4):
|
# Concurrency cap for parallel multimodal calls (default 4):
|
||||||
# EVEROS_MULTIMODAL__MAX_CONCURRENCY=4
|
# EVEROS_MULTIMODAL__MAX_CONCURRENCY=4
|
||||||
#
|
#
|
||||||
|
|||||||
@ -2,20 +2,30 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from everalgo.llm.types import (
|
||||||
|
ChatMessage,
|
||||||
|
ChatResponse,
|
||||||
|
ImageUrlInner,
|
||||||
|
ImageUrlPart,
|
||||||
|
TextPart,
|
||||||
|
)
|
||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
|
|
||||||
from everos.component.llm import LLMNotConfiguredError
|
from everos.component.llm import LLMNotConfiguredError
|
||||||
from everos.config import Settings
|
from everos.config import Settings
|
||||||
from everos.config.settings import LLMSettings
|
from everos.config.settings import LLMSettings, MultimodalSettings
|
||||||
|
|
||||||
_client_mod = importlib.import_module("everos.component.llm.client")
|
_client_mod = importlib.import_module("everos.component.llm.client")
|
||||||
|
|
||||||
|
|
||||||
def _reset_singleton(monkeypatch: pytest.MonkeyPatch) -> None:
|
def _reset_singleton(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(_client_mod, "_llm_client", None, raising=False)
|
monkeypatch.setattr(_client_mod, "_llm_client", None, raising=False)
|
||||||
|
monkeypatch.setattr(_client_mod, "_multimodal_client", None, raising=False)
|
||||||
|
|
||||||
|
|
||||||
def _patch_settings(
|
def _patch_settings(
|
||||||
@ -23,6 +33,7 @@ def _patch_settings(
|
|||||||
*,
|
*,
|
||||||
api_key: str | None,
|
api_key: str | None,
|
||||||
base_url: str | None,
|
base_url: str | None,
|
||||||
|
timeout_seconds: float | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stub the ``load_settings`` reference bound inside the client module."""
|
"""Stub the ``load_settings`` reference bound inside the client module."""
|
||||||
cfg = Settings(
|
cfg = Settings(
|
||||||
@ -30,11 +41,86 @@ def _patch_settings(
|
|||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
api_key=SecretStr(api_key) if api_key is not None else None,
|
api_key=SecretStr(api_key) if api_key is not None else None,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
**(
|
||||||
|
{"timeout_seconds": timeout_seconds}
|
||||||
|
if timeout_seconds is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(_client_mod, "load_settings", lambda: cfg)
|
monkeypatch.setattr(_client_mod, "load_settings", lambda: cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_multimodal_settings(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
*,
|
||||||
|
api_key: str | None,
|
||||||
|
base_url: str | None,
|
||||||
|
timeout_seconds: float | None = None,
|
||||||
|
resize_images_for_vlm: bool | None = None,
|
||||||
|
) -> None:
|
||||||
|
cfg = Settings(
|
||||||
|
multimodal=MultimodalSettings(
|
||||||
|
model="vision-model",
|
||||||
|
api_key=SecretStr(api_key) if api_key is not None else None,
|
||||||
|
base_url=base_url,
|
||||||
|
**(
|
||||||
|
{"timeout_seconds": timeout_seconds}
|
||||||
|
if timeout_seconds is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
**(
|
||||||
|
{"resize_images_for_vlm": resize_images_for_vlm}
|
||||||
|
if resize_images_for_vlm is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(_client_mod, "load_settings", lambda: cfg)
|
||||||
|
|
||||||
|
|
||||||
|
class _CapturingLLM:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.messages: list[ChatMessage] | None = None
|
||||||
|
self.kwargs: dict[str, object] | None = None
|
||||||
|
|
||||||
|
async def chat(
|
||||||
|
self,
|
||||||
|
messages: list[ChatMessage],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> ChatResponse:
|
||||||
|
self.messages = messages
|
||||||
|
self.kwargs = kwargs
|
||||||
|
return ChatResponse(content="ok", model="fake")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_no_thinking_param(kwargs: dict[str, object] | None) -> None:
|
||||||
|
assert kwargs is not None
|
||||||
|
extra_body = kwargs.get("extra_body")
|
||||||
|
assert isinstance(extra_body, dict)
|
||||||
|
chat_template_kwargs = extra_body.get("chat_template_kwargs")
|
||||||
|
assert isinstance(chat_template_kwargs, dict)
|
||||||
|
assert chat_template_kwargs["enable_thinking"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def _png_data_url(size: tuple[int, int]) -> str:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
image = Image.new("RGB", size, color=(255, 0, 0))
|
||||||
|
buffer = BytesIO()
|
||||||
|
image.save(buffer, format="PNG")
|
||||||
|
encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
|
||||||
|
return f"data:image/png;base64,{encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
def _data_url_image_size(data_url: str) -> tuple[int, int]:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
_, encoded = data_url.split(",", 1)
|
||||||
|
with Image.open(BytesIO(base64.b64decode(encoded))) as image:
|
||||||
|
return image.size
|
||||||
|
|
||||||
|
|
||||||
def test_raises_when_api_key_missing(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_raises_when_api_key_missing(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
_reset_singleton(monkeypatch)
|
_reset_singleton(monkeypatch)
|
||||||
_patch_settings(monkeypatch, api_key=None, base_url="https://example.test")
|
_patch_settings(monkeypatch, api_key=None, base_url="https://example.test")
|
||||||
@ -60,5 +146,295 @@ def test_returns_singleton_when_configured(monkeypatch: pytest.MonkeyPatch) -> N
|
|||||||
first = _client_mod.get_llm_client()
|
first = _client_mod.get_llm_client()
|
||||||
second = _client_mod.get_llm_client()
|
second = _client_mod.get_llm_client()
|
||||||
|
|
||||||
assert first is sentinel
|
|
||||||
assert first is second
|
assert first is second
|
||||||
|
assert first._inner is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_llm_client_defaults_to_no_thinking_param(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_settings(monkeypatch, api_key="sk-test", base_url="https://example.test")
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
|
||||||
|
client = _client_mod.get_llm_client()
|
||||||
|
await client.chat([ChatMessage(role="user", content="hello")])
|
||||||
|
|
||||||
|
_assert_no_thinking_param(captured.kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_llm_client_passes_configured_timeout(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
timeout_seconds=180.0,
|
||||||
|
)
|
||||||
|
captured_configs = []
|
||||||
|
sentinel = object()
|
||||||
|
|
||||||
|
def capture_build_client(cfg):
|
||||||
|
captured_configs.append(cfg)
|
||||||
|
return sentinel
|
||||||
|
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", capture_build_client)
|
||||||
|
|
||||||
|
client = _client_mod.get_llm_client()
|
||||||
|
assert client._inner is sentinel
|
||||||
|
assert captured_configs[0].timeout == 180.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_multimodal_client_passes_configured_timeout(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
timeout_seconds=240.0,
|
||||||
|
)
|
||||||
|
captured_configs = []
|
||||||
|
sentinel = _CapturingLLM()
|
||||||
|
|
||||||
|
def capture_build_client(cfg):
|
||||||
|
captured_configs.append(cfg)
|
||||||
|
return sentinel
|
||||||
|
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", capture_build_client)
|
||||||
|
|
||||||
|
_client_mod.get_multimodal_llm_client()
|
||||||
|
assert captured_configs[0].timeout == 240.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_sets_default_image_detail(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="describe"),
|
||||||
|
ImageUrlPart(
|
||||||
|
image_url=ImageUrlInner(url="data:image/jpeg;base64,abcd")
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat([original], max_tokens=10)
|
||||||
|
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_image = sent_content[1]
|
||||||
|
assert isinstance(sent_image, ImageUrlPart)
|
||||||
|
assert sent_image.image_url.detail == "auto"
|
||||||
|
|
||||||
|
original_content = original.content
|
||||||
|
assert isinstance(original_content, list)
|
||||||
|
original_image = original_content[1]
|
||||||
|
assert isinstance(original_image, ImageUrlPart)
|
||||||
|
assert original_image.image_url.detail is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_adds_visual_memory_instructions(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="Read this image and return its content."),
|
||||||
|
ImageUrlPart(
|
||||||
|
image_url=ImageUrlInner(url="data:image/jpeg;base64,abcd")
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat([original], max_tokens=10)
|
||||||
|
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_text = sent_content[0]
|
||||||
|
assert isinstance(sent_text, TextPart)
|
||||||
|
sent_text_lower = sent_text.text.lower()
|
||||||
|
assert "spatial relationships" in sent_text_lower
|
||||||
|
assert "relative positions" in sent_text_lower
|
||||||
|
assert "Do NOT describe the parser, assistant, or ChatGPT" in sent_text.text
|
||||||
|
|
||||||
|
original_content = original.content
|
||||||
|
assert isinstance(original_content, list)
|
||||||
|
original_text = original_content[0]
|
||||||
|
assert isinstance(original_text, TextPart)
|
||||||
|
assert "spatial relationships" not in original_text.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_defaults_to_no_thinking_param(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="Read this image and return its content."),
|
||||||
|
ImageUrlPart(
|
||||||
|
image_url=ImageUrlInner(url="data:image/jpeg;base64,abcd")
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat(
|
||||||
|
[original],
|
||||||
|
max_tokens=10,
|
||||||
|
extra_body={"provider": {"only": ["test"]}},
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_no_thinking_param(captured.kwargs)
|
||||||
|
assert captured.kwargs is not None
|
||||||
|
extra_body = captured.kwargs["extra_body"]
|
||||||
|
assert isinstance(extra_body, dict)
|
||||||
|
assert extra_body["provider"] == {"only": ["test"]}
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_text = sent_content[0]
|
||||||
|
assert isinstance(sent_text, TextPart)
|
||||||
|
assert "/no_think" not in sent_text.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_resizes_landscape_image_to_64_min_side_by_default(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
image_url = _png_data_url((640, 480))
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="describe"),
|
||||||
|
ImageUrlPart(image_url=ImageUrlInner(url=image_url)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat([original], max_tokens=10)
|
||||||
|
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_image = sent_content[1]
|
||||||
|
assert isinstance(sent_image, ImageUrlPart)
|
||||||
|
assert _data_url_image_size(sent_image.image_url.url) == (85, 64)
|
||||||
|
assert _data_url_image_size(image_url) == (640, 480)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_resizes_portrait_image_to_64_min_side_by_default(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
image_url = _png_data_url((480, 640))
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="describe"),
|
||||||
|
ImageUrlPart(image_url=ImageUrlInner(url=image_url)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat([original], max_tokens=10)
|
||||||
|
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_image = sent_content[1]
|
||||||
|
assert isinstance(sent_image, ImageUrlPart)
|
||||||
|
assert _data_url_image_size(sent_image.image_url.url) == (64, 85)
|
||||||
|
assert _data_url_image_size(image_url) == (480, 640)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multimodal_client_keeps_image_when_resize_disabled(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
_reset_singleton(monkeypatch)
|
||||||
|
_patch_multimodal_settings(
|
||||||
|
monkeypatch,
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://example.test",
|
||||||
|
resize_images_for_vlm=False,
|
||||||
|
)
|
||||||
|
captured = _CapturingLLM()
|
||||||
|
monkeypatch.setattr(_client_mod, "build_client", lambda cfg: captured)
|
||||||
|
image_url = _png_data_url((640, 480))
|
||||||
|
|
||||||
|
client = _client_mod.get_multimodal_llm_client()
|
||||||
|
original = ChatMessage(
|
||||||
|
role="user",
|
||||||
|
content=[
|
||||||
|
TextPart(text="describe"),
|
||||||
|
ImageUrlPart(image_url=ImageUrlInner(url=image_url)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.chat([original], max_tokens=10)
|
||||||
|
|
||||||
|
assert captured.messages is not None
|
||||||
|
sent_content = captured.messages[0].content
|
||||||
|
assert isinstance(sent_content, list)
|
||||||
|
sent_image = sent_content[1]
|
||||||
|
assert isinstance(sent_image, ImageUrlPart)
|
||||||
|
assert sent_image.image_url.url == image_url
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import pytest
|
|||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
|
|
||||||
from everos.component.llm import build_llm_provider
|
from everos.component.llm import build_llm_provider
|
||||||
|
from everos.component.llm import factory as factory_mod
|
||||||
from everos.component.llm.openai_provider import OpenAIProvider
|
from everos.component.llm.openai_provider import OpenAIProvider
|
||||||
from everos.config.settings import LLMSettings
|
from everos.config.settings import LLMSettings
|
||||||
|
|
||||||
@ -26,3 +27,23 @@ def test_builds_openai_provider() -> None:
|
|||||||
s = LLMSettings(model="m", api_key=SecretStr("k"), base_url="https://x")
|
s = LLMSettings(model="m", api_key=SecretStr("k"), base_url="https://x")
|
||||||
p = build_llm_provider(s)
|
p = build_llm_provider(s)
|
||||||
assert isinstance(p, OpenAIProvider)
|
assert isinstance(p, OpenAIProvider)
|
||||||
|
|
||||||
|
|
||||||
|
def test_passes_configured_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
captured_kwargs = {}
|
||||||
|
sentinel = object()
|
||||||
|
|
||||||
|
def capture_provider(**kwargs):
|
||||||
|
captured_kwargs.update(kwargs)
|
||||||
|
return sentinel
|
||||||
|
|
||||||
|
monkeypatch.setattr(factory_mod, "OpenAIProvider", capture_provider)
|
||||||
|
s = LLMSettings(
|
||||||
|
model="m",
|
||||||
|
api_key=SecretStr("k"),
|
||||||
|
base_url="https://x",
|
||||||
|
timeout_seconds=240.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert build_llm_provider(s) is sentinel
|
||||||
|
assert captured_kwargs["timeout"] == 240.0
|
||||||
|
|||||||
@ -105,6 +105,9 @@ def test_embedding_rerank_defaults() -> None:
|
|||||||
assert s.embedding.model is None
|
assert s.embedding.model is None
|
||||||
assert s.embedding.api_key is None
|
assert s.embedding.api_key is None
|
||||||
assert s.embedding.base_url is None
|
assert s.embedding.base_url is None
|
||||||
|
assert s.llm.timeout_seconds == 180.0
|
||||||
|
assert s.multimodal.timeout_seconds == 180.0
|
||||||
|
assert s.multimodal.resize_images_for_vlm is True
|
||||||
# Runtime knobs come from default.toml.
|
# Runtime knobs come from default.toml.
|
||||||
assert s.embedding.timeout_seconds == 30.0
|
assert s.embedding.timeout_seconds == 30.0
|
||||||
assert s.embedding.max_retries == 3
|
assert s.embedding.max_retries == 3
|
||||||
@ -126,6 +129,16 @@ def test_embedding_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
assert s.embedding.batch_size == 32
|
assert s.embedding.batch_size == 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_llm_timeout_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("EVEROS_LLM__TIMEOUT_SECONDS", "240")
|
||||||
|
monkeypatch.setenv("EVEROS_MULTIMODAL__TIMEOUT_SECONDS", "300")
|
||||||
|
monkeypatch.setenv("EVEROS_MULTIMODAL__RESIZE_IMAGES_FOR_VLM", "false")
|
||||||
|
s = Settings()
|
||||||
|
assert s.llm.timeout_seconds == 240.0
|
||||||
|
assert s.multimodal.timeout_seconds == 300.0
|
||||||
|
assert s.multimodal.resize_images_for_vlm is False
|
||||||
|
|
||||||
|
|
||||||
def test_rerank_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_rerank_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setenv("EVEROS_RERANK__MODEL", "BAAI/bge-reranker-v2-m3")
|
monkeypatch.setenv("EVEROS_RERANK__MODEL", "BAAI/bge-reranker-v2-m3")
|
||||||
monkeypatch.setenv("EVEROS_RERANK__MAX_CONCURRENT", "8")
|
monkeypatch.setenv("EVEROS_RERANK__MAX_CONCURRENT", "8")
|
||||||
|
|||||||
@ -41,6 +41,9 @@ def test_default_writes_dotenv_in_cwd(runner: CliRunner, in_tmp: Path) -> None:
|
|||||||
assert written.exists()
|
assert written.exists()
|
||||||
assert written.stat().st_size > 0
|
assert written.stat().st_size > 0
|
||||||
assert "EVEROS_LLM__API_KEY" in written.read_text()
|
assert "EVEROS_LLM__API_KEY" in written.read_text()
|
||||||
|
assert "https://github.com/EverMind-AI/EverOS/blob/main/QUICKSTART.md" in (
|
||||||
|
result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_default_file_permissions_are_0600(runner: CliRunner, in_tmp: Path) -> None:
|
def test_default_file_permissions_are_0600(runner: CliRunner, in_tmp: Path) -> None:
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from everos.core.persistence import MemoryRoot
|
from everos.core.persistence import MemoryRoot
|
||||||
|
from everos.memory.cascade import scanner as scanner_module
|
||||||
from everos.memory.cascade.scanner import CascadeScanner, _collect_scan_inputs
|
from everos.memory.cascade.scanner import CascadeScanner, _collect_scan_inputs
|
||||||
|
|
||||||
|
|
||||||
@ -112,16 +113,26 @@ async def test_run_loop_swallows_scan_exception(
|
|||||||
scanner = CascadeScanner(mr, scan_interval_seconds=0.05)
|
scanner = CascadeScanner(mr, scan_interval_seconds=0.05)
|
||||||
|
|
||||||
call_count = {"n": 0}
|
call_count = {"n": 0}
|
||||||
|
second_scan = asyncio.Event()
|
||||||
|
logged_errors: list[str] = []
|
||||||
|
|
||||||
async def fake_scan() -> list: # type: ignore[type-arg]
|
async def fake_scan() -> list: # type: ignore[type-arg]
|
||||||
call_count["n"] += 1
|
call_count["n"] += 1
|
||||||
if call_count["n"] == 1:
|
if call_count["n"] == 1:
|
||||||
raise RuntimeError("simulated scanner failure")
|
raise RuntimeError("simulated scanner failure")
|
||||||
|
second_scan.set()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def fake_exception(_event: str, *, error: str) -> None:
|
||||||
|
logged_errors.append(error)
|
||||||
|
|
||||||
monkeypatch.setattr(scanner, "scan_once", fake_scan)
|
monkeypatch.setattr(scanner, "scan_once", fake_scan)
|
||||||
|
monkeypatch.setattr(scanner_module.logger, "exception", fake_exception)
|
||||||
await scanner.start()
|
await scanner.start()
|
||||||
# Let the loop iterate at least twice (interval is 50ms).
|
try:
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.wait_for(second_scan.wait(), timeout=1.0)
|
||||||
await scanner.stop()
|
finally:
|
||||||
|
await scanner.stop()
|
||||||
|
|
||||||
|
assert logged_errors == ["simulated scanner failure"]
|
||||||
assert call_count["n"] >= 2 # second call ran despite first throwing
|
assert call_count["n"] >= 2 # second call ran despite first throwing
|
||||||
|
|||||||
@ -9,7 +9,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from everos.memory.cascade.watcher import _relative_to_root, _safe_mtime
|
from everos.memory.cascade.watcher import (
|
||||||
|
_relative_to_root,
|
||||||
|
_safe_mtime,
|
||||||
|
_watch_roots,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_relative_to_root_within(tmp_path: Path) -> None:
|
def test_relative_to_root_within(tmp_path: Path) -> None:
|
||||||
@ -34,3 +38,14 @@ def test_safe_mtime_existing_path_returns_positive(tmp_path: Path) -> None:
|
|||||||
f = tmp_path / "f.md"
|
f = tmp_path / "f.md"
|
||||||
f.write_text("ok")
|
f.write_text("ok")
|
||||||
assert _safe_mtime(str(f)) > 0
|
assert _safe_mtime(str(f)) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_watch_roots_excludes_system_dot_dirs(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / ".index" / "lancedb" / "episode").mkdir(parents=True)
|
||||||
|
(tmp_path / ".tmp").mkdir()
|
||||||
|
(tmp_path / "default_app" / "default_project" / "users").mkdir(parents=True)
|
||||||
|
(tmp_path / "default_app" / "default_project" / "agents").mkdir()
|
||||||
|
|
||||||
|
roots = _watch_roots(tmp_path)
|
||||||
|
|
||||||
|
assert roots == [tmp_path / "default_app"]
|
||||||
|
|||||||
@ -21,7 +21,10 @@ def test_derive_text_renders_parsed_nontext_as_tag() -> None:
|
|||||||
]
|
]
|
||||||
text, non_text = derive_text(items)
|
text, non_text = derive_text(items)
|
||||||
|
|
||||||
assert "[IMAGE: p.png]\nOCR TEXT" in text
|
assert "[IMAGE: p.png]" in text
|
||||||
|
assert "image visual facts" in text
|
||||||
|
assert "not assistant actions" in text
|
||||||
|
assert text.index("image visual facts") < text.index("OCR TEXT")
|
||||||
assert text.startswith("before")
|
assert text.startswith("before")
|
||||||
assert text.endswith("after")
|
assert text.endswith("after")
|
||||||
assert non_text == 0
|
assert non_text == 0
|
||||||
|
|||||||
@ -163,7 +163,7 @@ async def test_merges_into_existing_cluster_when_algo_matches() -> None:
|
|||||||
preview=["earlier intent"],
|
preview=["earlier intent"],
|
||||||
members=["ac_20260517_0000"],
|
members=["ac_20260517_0000"],
|
||||||
)
|
)
|
||||||
# Simulate evercore _merge: id passes through from existing, members appended.
|
# Simulate merge behavior: id passes through from existing, members appended.
|
||||||
merged_cluster = AlgoCluster(
|
merged_cluster = AlgoCluster(
|
||||||
id="cl_existing0001",
|
id="cl_existing0001",
|
||||||
centroid=np.array([0.17] * 1024, dtype=np.float32),
|
centroid=np.array([0.17] * 1024, dtype=np.float32),
|
||||||
|
|||||||
59
tests/unit/test_scripts/test_check_deprecated_names.py
Normal file
59
tests/unit/test_scripts/test_check_deprecated_names.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Self-tests for ``scripts/check_deprecated_names.py``."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
_CHECKER_PATH = _REPO_ROOT / "scripts" / "check_deprecated_names.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_checker():
|
||||||
|
assert _CHECKER_PATH.exists(), "deprecated-name checker should exist"
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"_deprecated_name_checker", _CHECKER_PATH
|
||||||
|
)
|
||||||
|
assert spec and spec.loader
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_text_is_allowed() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
violations = checker.find_violations(
|
||||||
|
[("README.md", "EverOS is the public project name.\n")]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert violations == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_name_variants_are_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
compact_name = "Ever" + "Core"
|
||||||
|
spaced_name = "ever" + " core"
|
||||||
|
hyphenated_name = "ever" + "-core"
|
||||||
|
|
||||||
|
violations = checker.find_violations(
|
||||||
|
[
|
||||||
|
("README.md", f"{compact_name} should not appear.\n"),
|
||||||
|
("docs/example.md", f"{spaced_name} should not appear.\n"),
|
||||||
|
("src/example.py", f"{hyphenated_name} should not appear.\n"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [(violation.path, violation.line_number) for violation in violations] == [
|
||||||
|
("README.md", 1),
|
||||||
|
("docs/example.md", 1),
|
||||||
|
("src/example.py", 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_repo_has_no_deprecated_names() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
assert checker.main() == 0
|
||||||
56
tests/unit/test_scripts/test_check_pr_title.py
Normal file
56
tests/unit/test_scripts/test_check_pr_title.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""Self-tests for ``scripts/check_pr_title.py``."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
_CHECKER_PATH = _REPO_ROOT / "scripts" / "check_pr_title.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_checker():
|
||||||
|
assert _CHECKER_PATH.exists(), "PR title checker should exist"
|
||||||
|
spec = importlib.util.spec_from_file_location("_pr_title_checker", _CHECKER_PATH)
|
||||||
|
assert spec and spec.loader
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def test_conventional_pr_title_is_allowed() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
assert checker.validate_title("docs(readme): polish launch highlights") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_bracketed_codex_pr_title_is_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
failures = checker.validate_title("[codex] simplify README launch highlights")
|
||||||
|
|
||||||
|
assert len(failures) == 1
|
||||||
|
assert "invalid PR title" in failures[0]
|
||||||
|
assert "expected: <type>[(scope)][!]: <description>" in failures[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_pr_title_is_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
title = (
|
||||||
|
"docs(readme): polish launch highlights and banner with a title that is "
|
||||||
|
"too long"
|
||||||
|
)
|
||||||
|
|
||||||
|
failures = checker.validate_title(title)
|
||||||
|
|
||||||
|
assert len(failures) == 1
|
||||||
|
assert "max is 72" in failures[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_reads_pr_title_environment(monkeypatch) -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
monkeypatch.setenv("PR_TITLE", "docs(readme): polish launch highlights")
|
||||||
|
|
||||||
|
assert checker.main([]) == 0
|
||||||
80
tests/unit/test_scripts/test_check_repo_assets.py
Normal file
80
tests/unit/test_scripts/test_check_repo_assets.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""Self-tests for ``scripts/check_repo_assets.py``."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
_CHECKER_PATH = _REPO_ROOT / "scripts" / "check_repo_assets.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_checker():
|
||||||
|
assert _CHECKER_PATH.exists(), "repo asset checker should exist"
|
||||||
|
spec = importlib.util.spec_from_file_location("_repo_asset_checker", _CHECKER_PATH)
|
||||||
|
assert spec and spec.loader
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_source_and_docs_paths_are_allowed() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
violations = checker.find_violations(
|
||||||
|
[
|
||||||
|
"README.md",
|
||||||
|
"docs/engineering.md",
|
||||||
|
"src/everos/__init__.py",
|
||||||
|
"use-cases/claude-code-plugin/dashboard/dashboard.html",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert violations == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_extensions_are_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
violations = checker.find_violations(["docs/banner.png", "icons/logo.svg"])
|
||||||
|
|
||||||
|
assert [violation.path for violation in violations] == [
|
||||||
|
"docs/banner.png",
|
||||||
|
"icons/logo.svg",
|
||||||
|
]
|
||||||
|
assert {violation.reason for violation in violations} == {"image file"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_extensions_are_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
violations = checker.find_violations(["demo/launch.mp4", "docs/clip.webm"])
|
||||||
|
|
||||||
|
assert [violation.path for violation in violations] == [
|
||||||
|
"demo/launch.mp4",
|
||||||
|
"docs/clip.webm",
|
||||||
|
]
|
||||||
|
assert {violation.reason for violation in violations} == {"video file"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_asset_and_media_directories_are_blocked() -> None:
|
||||||
|
checker = _load_checker()
|
||||||
|
|
||||||
|
violations = checker.find_violations(
|
||||||
|
[
|
||||||
|
"assets/banner.txt",
|
||||||
|
"docs/images/diagram.txt",
|
||||||
|
"use-cases/example/media/story.md",
|
||||||
|
"use-cases/example/videos/walkthrough.md",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [violation.path for violation in violations] == [
|
||||||
|
"assets/banner.txt",
|
||||||
|
"docs/images/diagram.txt",
|
||||||
|
"use-cases/example/media/story.md",
|
||||||
|
"use-cases/example/videos/walkthrough.md",
|
||||||
|
]
|
||||||
|
assert {violation.reason for violation in violations} == {"asset/media directory"}
|
||||||
@ -92,7 +92,7 @@ Earth Online is a memory-aware productivity game that turns everyday planning in
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/golutra/golutra)
|
[](https://github.com/golutra/golutra)
|
||||||
|
|
||||||
#### Multi-Agent Orchestration Platform
|
#### Multi-Agent Orchestration Platform
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ Record, visualize, and explore your tasting journey through an immersive 3D star
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/kellyvv/OpenHer)
|
[](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
#### EverOS Open Her
|
#### EverOS Open Her
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ Ruminer brings persistent memory to a browser agent so it can carry personal con
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/nanxingw/EverMem)
|
[](https://github.com/nanxingw/EverMem)
|
||||||
|
|
||||||
#### EverMem Sync with EverOS
|
#### EverMem Sync with EverOS
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ MCO equips your primary agent with an agent team that can work together to solve
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/onenewborn/StudyBuddy-public)
|
[](https://github.com/onenewborn/StudyBuddy-public)
|
||||||
|
|
||||||
#### Study Buddy with Self-Evolving Memory
|
#### Study Buddy with Self-Evolving Memory
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ Empowering individuals with advanced memory support and daily assistance.
|
|||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/AlexL1024/NeuralConnect)
|
[](https://github.com/AlexL1024/NeuralConnect)
|
||||||
|
|
||||||
#### Memory-Driven Multi-Agent NPC Experience
|
#### Memory-Driven Multi-Agent NPC Experience
|
||||||
|
|
||||||
@ -229,13 +229,13 @@ A context-native AI wearable that listens to everyday life and converts conversa
|
|||||||
<tr>
|
<tr>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
[](https://github.com/EverMind-AI/EverOS/tree/0f49826ba0f9a94e1974c97614a46a68e0a08b52/evermemos-openclaw-plugin)
|
[](../docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
#### OpenClaw Agent Memory
|
#### Legacy OpenClaw Agent Memory
|
||||||
|
|
||||||
A 24/7 agent workflow with continuous learning memory across sessions.
|
Archived pre-1.0.0 plugin reference. New integrations should use the EverOS 1.0.0 API.
|
||||||
|
|
||||||
[Plugin](https://github.com/EverMind-AI/EverOS/tree/0f49826ba0f9a94e1974c97614a46a68e0a08b52/evermemos-openclaw-plugin)
|
[Learn more](../docs/migration-to-1.0.0.md)
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|||||||
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.
|
Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.
|
||||||
|
|
||||||
|
> Compatibility note: this folder documents a legacy EverMem Cloud
|
||||||
|
> plugin. It still uses the old cloud `/api/v1/memories/*` routes and
|
||||||
|
> should not be treated as the canonical local EverOS 1.0.0 OSS API.
|
||||||
|
> New integrations should follow
|
||||||
|
> [EverOS 1.0.0 migration notes](../../docs/migration-to-1.0.0.md) and
|
||||||
|
> [the API reference](../../docs/api.md).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -889,7 +896,7 @@ function getGroupsForKey(keyId) {
|
|||||||
// Proxy forwards to upstream API with Authorization header
|
// Proxy forwards to upstream API with Authorization header
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dashboard (`assets/dashboard.html`)
|
### Dashboard (`dashboard/dashboard.html`)
|
||||||
|
|
||||||
**Data Loading Flow:**
|
**Data Loading Flow:**
|
||||||
|
|
||||||
@ -1059,7 +1066,7 @@ evermem-plugin/
|
|||||||
│ ├── config.js # Configuration utilities
|
│ ├── config.js # Configuration utilities
|
||||||
│ ├── debug.js # Shared debug logging utility
|
│ ├── debug.js # Shared debug logging utility
|
||||||
│ └── groups-store.js # Local groups persistence
|
│ └── groups-store.js # Local groups persistence
|
||||||
├── assets/
|
├── dashboard/
|
||||||
│ └── dashboard.html # Memory Hub dashboard
|
│ └── dashboard.html # Memory Hub dashboard
|
||||||
├── server/
|
├── server/
|
||||||
│ └── proxy.js # Local proxy server for dashboard
|
│ └── proxy.js # Local proxy server for dashboard
|
||||||
|
|||||||
@ -58,4 +58,4 @@ export function debug(...args) {
|
|||||||
*/
|
*/
|
||||||
export function isDebugEnabled() {
|
export function isDebugEnabled() {
|
||||||
return DEBUG;
|
return DEBUG;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,7 @@ You have access to memory tools that can recall context from the user's past cod
|
|||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
|
|
||||||
- **search_memories**: Search past conversations using semantic + keyword matching
|
- **evermem_search**: Search past conversations using semantic + keyword matching. Params: `query` (required), `limit` (default 10, max 20)
|
||||||
- **get_memory**: Retrieve full details of a specific memory by ID
|
|
||||||
|
|
||||||
## When to Use Memory Search
|
## When to Use Memory Search
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# EverMem Story Memory Demo
|
# EverMem Story Memory Demo
|
||||||
|
|
||||||
> Built on [EverCore](https://github.com/EverMind-AI/EverOS/) - Open-source AI memory infrastructure
|
> Built on [EverOS](https://github.com/EverMind-AI/EverOS/) - Open-source AI memory infrastructure
|
||||||
|
|
||||||
A demonstration web application showcasing [EverMem](https://evermind.ai)'s AI memory infrastructure through an interactive Q&A experience with "A Game of Thrones" (Book 1).
|
A demonstration web application showcasing [EverMem](https://evermind.ai)'s AI memory infrastructure through an interactive Q&A experience with "A Game of Thrones" (Book 1).
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ Ask questions about the book and watch two AI responses stream side-by-side: one
|
|||||||
|
|
||||||
- [Bun](https://bun.sh/) (latest version)
|
- [Bun](https://bun.sh/) (latest version)
|
||||||
- OpenAI API key (or OpenRouter API key)
|
- OpenAI API key (or OpenRouter API key)
|
||||||
- EverMind Cloud API key (apply at [EverCore Cloud](https://console.evermind.ai/))
|
- EverMind Cloud API key (apply at [EverMind Cloud](https://console.evermind.ai/))
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { MockMemoryService } from './services/MockMemoryService.js';
|
import { MockMemoryService } from './services/MockMemoryService.js';
|
||||||
import { EverCoreService } from './services/EverMemOSService.js';
|
import { EverOSService } from './services/EverMemOSService.js';
|
||||||
import { OpenAIService } from './services/OpenAIService.js';
|
import { OpenAIService } from './services/OpenAIService.js';
|
||||||
import { createChatRouter } from './routes/chat.js';
|
import { createChatRouter } from './routes/chat.js';
|
||||||
import { createHealthRouter } from './routes/health.js';
|
import { createHealthRouter } from './routes/health.js';
|
||||||
@ -24,7 +24,7 @@ if (!OPENAI_API_KEY) {
|
|||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
const memoryService = USE_EVERMEMOS
|
const memoryService = USE_EVERMEMOS
|
||||||
? new EverCoreService({
|
? new EverOSService({
|
||||||
baseUrl: EVERMEMOS_URL,
|
baseUrl: EVERMEMOS_URL,
|
||||||
apiKey: EVERMEMOS_API_KEY || undefined,
|
apiKey: EVERMEMOS_API_KEY || undefined,
|
||||||
groupId: EVERMEMOS_GROUP_ID,
|
groupId: EVERMEMOS_GROUP_ID,
|
||||||
@ -33,9 +33,9 @@ const memoryService = USE_EVERMEMOS
|
|||||||
const openaiService = new OpenAIService(OPENAI_API_KEY, OPENAI_MODEL);
|
const openaiService = new OpenAIService(OPENAI_API_KEY, OPENAI_MODEL);
|
||||||
|
|
||||||
const isCloudMode = USE_EVERMEMOS && !!EVERMEMOS_API_KEY;
|
const isCloudMode = USE_EVERMEMOS && !!EVERMEMOS_API_KEY;
|
||||||
logger.info('Server', `Memory service: ${USE_EVERMEMOS ? (isCloudMode ? 'EverMind Cloud' : 'EverCore (local)') : 'Mock'}`);
|
logger.info('Server', `Memory service: ${USE_EVERMEMOS ? (isCloudMode ? 'EverMind Cloud' : 'EverOS (local)') : 'Mock'}`);
|
||||||
if (USE_EVERMEMOS) {
|
if (USE_EVERMEMOS) {
|
||||||
logger.info('Server', `EverCore URL: ${EVERMEMOS_URL}`);
|
logger.info('Server', `EverOS URL: ${EVERMEMOS_URL}`);
|
||||||
if (isCloudMode) {
|
if (isCloudMode) {
|
||||||
logger.info('Server', `EverMind Cloud API Key: ${EVERMEMOS_API_KEY.slice(0, 8)}...`);
|
logger.info('Server', `EverMind Cloud API Key: ${EVERMEMOS_API_KEY.slice(0, 8)}...`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { IMemoryService, Memory } from './IMemoryService';
|
import type { IMemoryService, Memory } from './IMemoryService';
|
||||||
|
|
||||||
interface EverCoreMemoryItem {
|
interface EverOSMemoryItem {
|
||||||
memory_type: string;
|
memory_type: string;
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
subject?: string; // Concise title/headline
|
subject?: string; // Concise title/headline
|
||||||
@ -38,12 +38,12 @@ interface ProfileSearchItem {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EverCoreSearchResponse {
|
interface EverOSSearchResponse {
|
||||||
status: string;
|
status: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
result: {
|
result: {
|
||||||
profiles: ProfileSearchItem[];
|
profiles: ProfileSearchItem[];
|
||||||
memories: EverCoreMemoryItem[];
|
memories: EverOSMemoryItem[];
|
||||||
total_count: number;
|
total_count: number;
|
||||||
scores: number[];
|
scores: number[];
|
||||||
has_more: boolean;
|
has_more: boolean;
|
||||||
@ -53,7 +53,7 @@ interface EverCoreSearchResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EverCoreHealthResponse {
|
interface EverOSHealthResponse {
|
||||||
status: string;
|
status: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@ -70,25 +70,25 @@ const BOOK_TITLES: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for EverCore/EverMind Cloud service
|
* Configuration for EverOS/EverMind Cloud service
|
||||||
*/
|
*/
|
||||||
interface EverCoreConfig {
|
interface EverOSConfig {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey?: string; // Required for cloud API
|
apiKey?: string; // Required for cloud API
|
||||||
groupId?: string; // Group ID for search (default: 'asoiaf')
|
groupId?: string; // Group ID for search (default: 'asoiaf')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EverCore service implementation for memory retrieval
|
* EverOS service implementation for memory retrieval
|
||||||
* Supports both local EverCore and EverMind Cloud API
|
* Supports both local EverOS and EverMind Cloud API
|
||||||
*/
|
*/
|
||||||
export class EverCoreService implements IMemoryService {
|
export class EverOSService implements IMemoryService {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
private groupId: string;
|
private groupId: string;
|
||||||
private isCloudMode: boolean;
|
private isCloudMode: boolean;
|
||||||
|
|
||||||
constructor(config: string | EverCoreConfig) {
|
constructor(config: string | EverOSConfig) {
|
||||||
if (typeof config === 'string') {
|
if (typeof config === 'string') {
|
||||||
// Legacy: just a URL string (local mode)
|
// Legacy: just a URL string (local mode)
|
||||||
this.baseUrl = config.replace(/\/$/, '');
|
this.baseUrl = config.replace(/\/$/, '');
|
||||||
@ -104,7 +104,7 @@ export class EverCoreService implements IMemoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve relevant memories for a query using EverCore search
|
* Retrieve relevant memories for a query using EverOS search
|
||||||
*/
|
*/
|
||||||
async retrieveMemories(query: string, limit: number = 5): Promise<Memory[]> {
|
async retrieveMemories(query: string, limit: number = 5): Promise<Memory[]> {
|
||||||
try {
|
try {
|
||||||
@ -136,20 +136,20 @@ export class EverCoreService implements IMemoryService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`EverCore search failed: HTTP ${response.status}`);
|
console.error(`EverOS search failed: HTTP ${response.status}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json() as EverCoreSearchResponse;
|
const data = await response.json() as EverOSSearchResponse;
|
||||||
return this.mapSearchResultsToMemories(data);
|
return this.mapSearchResultsToMemories(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrieving memories from EverCore:', error);
|
console.error('Error retrieving memories from EverOS:', error);
|
||||||
return []; // Graceful degradation
|
return []; // Graceful degradation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if EverCore service is available
|
* Check if EverOS service is available
|
||||||
*/
|
*/
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@ -168,19 +168,19 @@ export class EverCoreService implements IMemoryService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json() as EverCoreHealthResponse;
|
const data = await response.json() as EverOSHealthResponse;
|
||||||
// Cloud API returns "ok" status, local returns "healthy"
|
// Cloud API returns "ok" status, local returns "healthy"
|
||||||
return data.status === 'healthy' || data.status === 'ok';
|
return data.status === 'healthy' || data.status === 'ok';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('EverCore health check failed:', error);
|
console.warn('EverOS health check failed:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map EverCore search results to our Memory interface
|
* Map EverOS search results to our Memory interface
|
||||||
*/
|
*/
|
||||||
private mapSearchResultsToMemories(data: EverCoreSearchResponse): Memory[] {
|
private mapSearchResultsToMemories(data: EverOSSearchResponse): Memory[] {
|
||||||
const memories: Memory[] = [];
|
const memories: Memory[] = [];
|
||||||
|
|
||||||
const result = data.result;
|
const result = data.result;
|
||||||
@ -206,10 +206,10 @@ export class EverCoreService implements IMemoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map a single EverCore memory item to our Memory interface
|
* Map a single EverOS memory item to our Memory interface
|
||||||
*/
|
*/
|
||||||
private mapMemoryItem(
|
private mapMemoryItem(
|
||||||
item: EverCoreMemoryItem,
|
item: EverOSMemoryItem,
|
||||||
score: number,
|
score: number,
|
||||||
originalContents: OriginalDataItem[] = []
|
originalContents: OriginalDataItem[] = []
|
||||||
): Memory | null {
|
): Memory | null {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# OpenHer × EverCore Use Case
|
# OpenHer × EverOS Use Case
|
||||||
# Copy this file to .env and fill in your values
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
# ─── LLM Provider (pick one) ───
|
# ─── LLM Provider (pick one) ───
|
||||||
@ -17,12 +17,12 @@ GEMINI_API_KEY=your_gemini_api_key_here
|
|||||||
# OpenAI (alternative)
|
# OpenAI (alternative)
|
||||||
# OPENAI_API_KEY=your_openai_api_key_here
|
# OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
# ─── EverCore Long-Term Memory ───
|
# ─── EverOS Long-Term Memory ───
|
||||||
|
|
||||||
# Option A: EverCore Cloud
|
# Option A: EverMind Cloud
|
||||||
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||||
EVERMEMOS_API_KEY=your_evermemos_api_key_here
|
EVERMEMOS_API_KEY=your_evermemos_api_key_here
|
||||||
|
|
||||||
# Option B: Self-Hosted EverCore
|
# Option B: Self-Hosted EverOS
|
||||||
# cd vendor/EverCore && docker compose up -d && uv run python src/run.py
|
# cd vendor/EverOS && docker compose up -d && uv run python src/run.py
|
||||||
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
# OpenHer — Teaching AI to Remember Who You Are
|
# OpenHer — Teaching AI to Remember Who You Are
|
||||||
|
|
||||||
Built on [EverCore](https://github.com/EverMind-AI/EverOS/tree/main/methods/EverCore) — Open-source AI memory infrastructure
|
Built on [EverOS](https://github.com/EverMind-AI/EverOS) — open-source AI memory infrastructure
|
||||||
|
|
||||||
**OpenHer** doesn't build chatbots. It doesn't build AI assistants. It builds **AI Beings** — entities with personality, emotion, and memory that *feel*, *remember*, and *grow* through every interaction.
|
**OpenHer** doesn't build chatbots. It doesn't build AI assistants. It builds **AI Beings** — entities with personality, emotion, and memory that *feel*, *remember*, and *grow* through every interaction.
|
||||||
|
|
||||||
**EverCore** is her long-term memory — the part that lets her carry your story across sessions, remember who you are, what you've talked about, and how your relationship has evolved.
|
**EverOS** is her long-term memory — the part that lets her carry your story across sessions, remember who you are, what you've talked about, and how your relationship has evolved.
|
||||||
|
|
||||||
Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
|||||||
|
|
||||||
Without memory, every conversation starts from zero. She doesn't know your name. She doesn't remember that three weeks ago you mentioned you drink your coffee black. She doesn't know you once had a fight and made up.
|
Without memory, every conversation starts from zero. She doesn't know your name. She doesn't remember that three weeks ago you mentioned you drink your coffee black. She doesn't know you once had a fight and made up.
|
||||||
|
|
||||||
With EverCore:
|
With EverOS:
|
||||||
|
|
||||||
**She remembers what you said.**
|
**She remembers what you said.**
|
||||||
Three weeks ago you casually mentioned no sugar in your coffee. Today she says: "Americano, no sugar, right?"
|
Three weeks ago you casually mentioned no sugar in your coffee. Today she says: "Americano, no sugar, right?"
|
||||||
@ -31,19 +31,19 @@ Last time you mentioned work stress. This time she asks: "How's that project goi
|
|||||||
|
|
||||||
## Memory Architecture
|
## Memory Architecture
|
||||||
|
|
||||||
OpenHer's memory has three layers. EverCore powers the deepest one:
|
OpenHer's memory has three layers. EverOS powers the deepest one:
|
||||||
|
|
||||||
| Layer | What it does | Analogy |
|
| Layer | What it does | Analogy |
|
||||||
|:------|:-------------|:--------|
|
|:------|:-------------|:--------|
|
||||||
| **Style Memory** | Her behavioral habits — tone, expression patterns | Muscle memory |
|
| **Style Memory** | Her behavioral habits — tone, expression patterns | Muscle memory |
|
||||||
| **Local Facts** | Your preferences, personal info | Short-term memory |
|
| **Local Facts** | Your preferences, personal info | Short-term memory |
|
||||||
| **Long-Term Memory** | What happened between you, her understanding of you, her hunches | **Episodic memory (EverCore)** |
|
| **Long-Term Memory** | What happened between you, her understanding of you, her hunches | **Episodic memory (EverOS)** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How Memory Feeds Into Personality
|
## How Memory Feeds Into Personality
|
||||||
|
|
||||||
OpenHer's core is a living neural network (25D input, 24D hidden, 8D behavioral signals). EverCore provides 4 key dimensions that let her tell the difference between a stranger and an old friend:
|
OpenHer's core is a living neural network (25D input, 24D hidden, 8D behavioral signals). EverOS provides 4 key dimensions that let her tell the difference between a stranger and an old friend:
|
||||||
|
|
||||||
```
|
```
|
||||||
Relationship Depth 0 ─────────────────── 1
|
Relationship Depth 0 ─────────────────── 1
|
||||||
@ -96,13 +96,13 @@ User sends a message
|
|||||||
|
|
|
|
||||||
v
|
v
|
||||||
Load memory -- First turn: load "who you are", "what we talked about",
|
Load memory -- First turn: load "who you are", "what we talked about",
|
||||||
| "what's on her mind" from EverCore
|
| "what's on her mind" from EverOS
|
||||||
v
|
v
|
||||||
Perceive -- LLM evaluates the current moment: your emotion, topic
|
Perceive -- LLM evaluates the current moment: your emotion, topic
|
||||||
| intimacy, conflict level... (8 dimensions)
|
| intimacy, conflict level... (8 dimensions)
|
||||||
| + relationship dimensions from EverCore (4 dimensions) = 12D
|
| + relationship dimensions from EverOS (4 dimensions) = 12D
|
||||||
v
|
v
|
||||||
Relationship evolves -- Blend EverCore history with this turn's changes
|
Relationship evolves -- Blend EverOS history with this turn's changes
|
||||||
| Smoothed so a single remark can't flip the relationship
|
| Smoothed so a single remark can't flip the relationship
|
||||||
v
|
v
|
||||||
Neural network -- 25D input (drives + context + relationship + internal state)
|
Neural network -- 25D input (drives + context + relationship + internal state)
|
||||||
@ -115,7 +115,7 @@ User sends a message
|
|||||||
Respond -- Internal monologue first, then choose what to say and how
|
Respond -- Internal monologue first, then choose what to say and how
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Remember this turn -- Store the conversation in EverCore (async, non-blocking)
|
Remember this turn -- Store the conversation in EverOS (async, non-blocking)
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Prepare for next -- Search for memories related to what you just said
|
Prepare for next -- Search for memories related to what you just said
|
||||||
@ -128,7 +128,7 @@ User sends a message
|
|||||||
- **Emergent Personality** — Not written in a prompt. Emerges from random neural networks, 5D drives, and Hebbian learning
|
- **Emergent Personality** — Not written in a prompt. Emerges from random neural networks, 5D drives, and Hebbian learning
|
||||||
- **Emotional Thermodynamics** — Drives metabolize over real time. She gets lonely when you're away, irritated when ignored
|
- **Emotional Thermodynamics** — Drives metabolize over real time. She gets lonely when you're away, irritated when ignored
|
||||||
- **Feel First** — Every response starts with an internal monologue before choosing words
|
- **Feel First** — Every response starts with an internal monologue before choosing words
|
||||||
- **Cross-Session Memory** — EverCore stores your shared story across every conversation
|
- **Cross-Session Memory** — EverOS stores your shared story across every conversation
|
||||||
- **Relationship Evolution** — The relationship vector deepens naturally with each turn
|
- **Relationship Evolution** — The relationship vector deepens naturally with each turn
|
||||||
- **Proactive Messages** — She reaches out not on a timer, but because her connection hunger is rising
|
- **Proactive Messages** — She reaches out not on a timer, but because her connection hunger is rising
|
||||||
- **Modal Expression** — She chooses text, voice, or photos based on what the moment calls for
|
- **Modal Expression** — She chooses text, voice, or photos based on what the moment calls for
|
||||||
@ -140,7 +140,7 @@ User sends a message
|
|||||||
|:------|:-----------|
|
|:------|:-----------|
|
||||||
| Runtime | Python 3.11+, FastAPI, WebSocket, asyncio |
|
| Runtime | Python 3.11+, FastAPI, WebSocket, asyncio |
|
||||||
| LLM | Gemini, Claude, Qwen3, GPT-5.4-mini, MiniMax, Moonshot, StepFun, Ollama |
|
| LLM | Gemini, Claude, Qwen3, GPT-5.4-mini, MiniMax, Moonshot, StepFun, Ollama |
|
||||||
| Memory | **EverCore** (self-hosted / cloud) + SQLite local state |
|
| Memory | **EverOS** (self-hosted / cloud) + SQLite local state |
|
||||||
| Desktop | SwiftUI (macOS native) |
|
| Desktop | SwiftUI (macOS native) |
|
||||||
| Voice | DashScope, OpenAI, MiniMax |
|
| Voice | DashScope, OpenAI, MiniMax |
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ User sends a message
|
|||||||
|
|
||||||
- Python 3.11+
|
- Python 3.11+
|
||||||
- Any supported LLM provider API key
|
- Any supported LLM provider API key
|
||||||
- EverCore (self-hosted or cloud)
|
- EverOS (self-hosted or cloud)
|
||||||
|
|
||||||
### 1. Clone & Install
|
### 1. Clone & Install
|
||||||
|
|
||||||
@ -174,12 +174,12 @@ DEFAULT_PROVIDER=gemini
|
|||||||
DEFAULT_MODEL=gemini-3.1-flash-lite-preview
|
DEFAULT_MODEL=gemini-3.1-flash-lite-preview
|
||||||
GEMINI_API_KEY=your_key
|
GEMINI_API_KEY=your_key
|
||||||
|
|
||||||
# EverCore — Cloud
|
# EverMind Cloud
|
||||||
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||||
EVERMEMOS_API_KEY=your_key
|
EVERMEMOS_API_KEY=your_key
|
||||||
|
|
||||||
# EverCore — Self-hosted
|
# EverOS — Self-hosted
|
||||||
# cd vendor/EverCore && docker compose up -d && uv run python src/run.py
|
# cd vendor/EverOS && docker compose up -d && uv run python src/run.py
|
||||||
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
# EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ python main.py
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python demo/evermemos_demo.py
|
python demo/evermemos_demo.py
|
||||||
# Runs in simulation mode even without EverCore
|
# Runs in simulation mode even without EverOS
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -205,11 +205,11 @@ python demo/evermemos_demo.py
|
|||||||
OpenHer/
|
OpenHer/
|
||||||
├── agent/
|
├── agent/
|
||||||
│ ├── chat_agent.py # Main agent, full lifecycle
|
│ ├── chat_agent.py # Main agent, full lifecycle
|
||||||
│ ├── evermemos_mixin.py # EverCore integration (load/store/search/EMA)
|
│ ├── evermemos_mixin.py # EverOS integration (load/store/search/EMA)
|
||||||
│ └── prompt_builder.py # Memory injection into Actor prompt
|
│ └── prompt_builder.py # Memory injection into Actor prompt
|
||||||
├── engine/
|
├── engine/
|
||||||
│ └── genome/
|
│ └── genome/
|
||||||
│ ├── genome_engine.py # Neural network + 12D context (incl. 4D EverCore)
|
│ ├── genome_engine.py # Neural network + 12D context (incl. 4D EverOS)
|
||||||
│ ├── critic.py # LLM perception: 8D context + relationship deltas
|
│ ├── critic.py # LLM perception: 8D context + relationship deltas
|
||||||
│ ├── drive_metabolism.py # Emotional thermodynamics
|
│ ├── drive_metabolism.py # Emotional thermodynamics
|
||||||
│ └── style_memory.py # KNN behavioral memory + Hawking radiation decay
|
│ └── style_memory.py # KNN behavioral memory + Hawking radiation decay
|
||||||
@ -219,7 +219,7 @@ OpenHer/
|
|||||||
├── persona/
|
├── persona/
|
||||||
│ └── personas/ # 10 pre-built personas (SOUL.md + seeds)
|
│ └── personas/ # 10 pre-built personas (SOUL.md + seeds)
|
||||||
├── vendor/
|
├── vendor/
|
||||||
│ └── EverCore/ # Self-hosted EverCore
|
│ └── EverOS/ # Self-hosted EverOS
|
||||||
└── main.py # FastAPI server
|
└── main.py # FastAPI server
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ OpenHer/
|
|||||||
|
|
||||||
## Integration Code at a Glance
|
## Integration Code at a Glance
|
||||||
|
|
||||||
### EverCore Mixin
|
### EverOS Mixin
|
||||||
|
|
||||||
The core integration is a mixin class handling four async operations:
|
The core integration is a mixin class handling four async operations:
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ class SessionContext:
|
|||||||
|
|
||||||
## Without Memory vs. With Memory
|
## Without Memory vs. With Memory
|
||||||
|
|
||||||
| | Without EverCore | With EverCore |
|
| | Without EverOS | With EverOS |
|
||||||
|:--|:--|:--|
|
|:--|:--|:--|
|
||||||
| First meeting | "Hi! I'm Luna" | "Hi! I'm Luna" |
|
| First meeting | "Hi! I'm Luna" | "Hi! I'm Luna" |
|
||||||
| Second meeting | "Hi! I'm Luna" | "Hey Alex! How's that project going?" |
|
| Second meeting | "Hi! I'm Luna" | "Hey Alex! How's that project going?" |
|
||||||
@ -278,7 +278,7 @@ class SessionContext:
|
|||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
- Full Project: [github.com/kellyvv/OpenHer](https://github.com/kellyvv/OpenHer)
|
||||||
- EverCore: [evermind.ai](https://evermind.ai)
|
- EverOS: [evermind.ai](https://evermind.ai)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
OpenHer × EverCore Integration Demo
|
OpenHer × EverOS Integration Demo
|
||||||
|
|
||||||
Demonstrates how EverCore provides long-term memory to the
|
Demonstrates how EverOS provides long-term memory to the
|
||||||
AI Being persona engine. Shows session context loading, memory
|
AI Being persona engine. Shows session context loading, memory
|
||||||
storage, search, and relationship vector evolution.
|
storage, search, and relationship vector evolution.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
# With EverCore Cloud
|
# With EverMind Cloud
|
||||||
export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1
|
||||||
export EVERMEMOS_API_KEY=your_key
|
export EVERMEMOS_API_KEY=your_key
|
||||||
python demo/evermemos_demo.py
|
python demo/evermemos_demo.py
|
||||||
|
|
||||||
# With self-hosted EverCore
|
# With self-hosted EverOS
|
||||||
export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1
|
||||||
python demo/evermemos_demo.py
|
python demo/evermemos_demo.py
|
||||||
"""
|
"""
|
||||||
@ -20,12 +20,10 @@ Usage:
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# EverCore Client (minimal standalone version)
|
# EverOS Client (minimal standalone version)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -35,8 +33,8 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
class EverCoreClient:
|
class EverOSClient:
|
||||||
"""Minimal EverCore client for demo purposes."""
|
"""Minimal EverOS client for demo purposes."""
|
||||||
|
|
||||||
def __init__(self, base_url: str, api_key: str = ""):
|
def __init__(self, base_url: str, api_key: str = ""):
|
||||||
self.base_url = base_url.rstrip("/")
|
self.base_url = base_url.rstrip("/")
|
||||||
@ -51,7 +49,7 @@ class EverCoreClient:
|
|||||||
return h
|
return h
|
||||||
|
|
||||||
async def health_check(self) -> bool:
|
async def health_check(self) -> bool:
|
||||||
"""Check if EverCore is reachable."""
|
"""Check if EverOS is reachable."""
|
||||||
try:
|
try:
|
||||||
# Try the health endpoint (remove /api/v1 suffix)
|
# Try the health endpoint (remove /api/v1 suffix)
|
||||||
health_url = self.base_url.replace("/api/v1", "") + "/health"
|
health_url = self.base_url.replace("/api/v1", "") + "/health"
|
||||||
@ -129,12 +127,13 @@ class EverCoreClient:
|
|||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# Relationship Vector (from EverCore session)
|
# Relationship Vector (from EverOS session)
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def compute_relationship_vector(profile_data: dict) -> dict:
|
def compute_relationship_vector(profile_data: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
Extract 4D relationship vector from EverCore profile data.
|
Extract 4D relationship vector from EverOS profile data.
|
||||||
|
|
||||||
These 4 dimensions expand the persona engine's neural network
|
These 4 dimensions expand the persona engine's neural network
|
||||||
from 8D to 12D input, allowing it to differentiate behavior
|
from 8D to 12D input, allowing it to differentiate behavior
|
||||||
@ -152,12 +151,12 @@ def apply_relationship_ema(
|
|||||||
prior: dict,
|
prior: dict,
|
||||||
delta: dict,
|
delta: dict,
|
||||||
conversation_depth: float,
|
conversation_depth: float,
|
||||||
prev_ema: Optional[dict] = None,
|
prev_ema: dict | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Semi-emergent relationship update (Step 2.5 of ChatAgent lifecycle).
|
Semi-emergent relationship update (Step 2.5 of ChatAgent lifecycle).
|
||||||
|
|
||||||
Blends EverCore prior with LLM-judged delta through EMA:
|
Blends EverOS prior with LLM-judged delta through EMA:
|
||||||
- alpha modulated by conversation depth (deeper = trust LLM more)
|
- alpha modulated by conversation depth (deeper = trust LLM more)
|
||||||
- Clips to valid ranges
|
- Clips to valid ranges
|
||||||
- Preserves momentum through prev_ema
|
- Preserves momentum through prev_ema
|
||||||
@ -181,25 +180,26 @@ def apply_relationship_ema(
|
|||||||
# Demo
|
# Demo
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
base_url = os.getenv("EVERMEMOS_BASE_URL", "")
|
base_url = os.getenv("EVERMEMOS_BASE_URL", "")
|
||||||
api_key = os.getenv("EVERMEMOS_API_KEY", "")
|
api_key = os.getenv("EVERMEMOS_API_KEY", "")
|
||||||
|
|
||||||
if not base_url:
|
if not base_url:
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("OpenHer × EverCore Integration Demo")
|
print("OpenHer × EverOS Integration Demo")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
print("⚠️ EVERMEMOS_BASE_URL not set.")
|
print("⚠️ EVERMEMOS_BASE_URL not set.")
|
||||||
print()
|
print()
|
||||||
print("To run this demo, set up EverCore:")
|
print("To run this demo, set up EverOS:")
|
||||||
print()
|
print()
|
||||||
print(" Option A — Cloud:")
|
print(" Option A — EverMind Cloud:")
|
||||||
print(" export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1")
|
print(" export EVERMEMOS_BASE_URL=https://api.evermind.ai/v1")
|
||||||
print(" export EVERMEMOS_API_KEY=your_key")
|
print(" export EVERMEMOS_API_KEY=your_key")
|
||||||
print()
|
print()
|
||||||
print(" Option B — Self-hosted:")
|
print(" Option B — Self-hosted:")
|
||||||
print(" cd vendor/EverCore && docker compose up -d")
|
print(" cd vendor/EverOS && docker compose up -d")
|
||||||
print(" uv run python src/run.py")
|
print(" uv run python src/run.py")
|
||||||
print(" export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1")
|
print(" export EVERMEMOS_BASE_URL=http://localhost:1995/api/v1")
|
||||||
print()
|
print()
|
||||||
@ -209,20 +209,20 @@ async def main():
|
|||||||
await demo_simulation()
|
await demo_simulation()
|
||||||
return
|
return
|
||||||
|
|
||||||
client = EverCoreClient(base_url, api_key)
|
client = EverOSClient(base_url, api_key)
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("OpenHer × EverCore Integration Demo")
|
print("OpenHer × EverOS Integration Demo")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f"\n📡 EverCore: {base_url}")
|
print(f"\n📡 EverOS: {base_url}")
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
healthy = await client.health_check()
|
healthy = await client.health_check()
|
||||||
if not healthy:
|
if not healthy:
|
||||||
print("❌ EverCore is not reachable. Check your URL and try again.")
|
print("❌ EverOS is not reachable. Check your URL and try again.")
|
||||||
await client.close()
|
await client.close()
|
||||||
return
|
return
|
||||||
print("✅ EverCore is healthy\n")
|
print("✅ EverOS is healthy\n")
|
||||||
|
|
||||||
# ── Demo conversation ──
|
# ── Demo conversation ──
|
||||||
user_id = "demo_user"
|
user_id = "demo_user"
|
||||||
@ -232,8 +232,15 @@ async def main():
|
|||||||
group_id = f"{persona_id}__{user_id}"
|
group_id = f"{persona_id}__{user_id}"
|
||||||
|
|
||||||
conversations = [
|
conversations = [
|
||||||
("My name is Alex, I'm a software engineer", "Nice to meet you Alex! What kind of software do you work on?"),
|
(
|
||||||
("I love hiking in the mountains on weekends", "That sounds wonderful! There's something about being up high that makes everything else feel small."),
|
"My name is Alex, I'm a software engineer",
|
||||||
|
"Nice to meet you Alex! What kind of software do you work on?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"I love hiking in the mountains on weekends",
|
||||||
|
"That sounds wonderful! There's something about being up high "
|
||||||
|
"that makes everything else feel small.",
|
||||||
|
),
|
||||||
("I drink my coffee black, no sugar", "Noted! A purist. I respect that."),
|
("I drink my coffee black, no sugar", "Noted! A purist. I respect that."),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -249,7 +256,7 @@ async def main():
|
|||||||
agent_reply=agent_reply,
|
agent_reply=agent_reply,
|
||||||
)
|
)
|
||||||
status = "✅" if "error" not in result else "❌"
|
status = "✅" if "error" not in result else "❌"
|
||||||
print(f" {status} User: \"{user_msg[:50]}...\"")
|
print(f' {status} User: "{user_msg[:50]}..."')
|
||||||
|
|
||||||
# Wait for indexing
|
# Wait for indexing
|
||||||
print("\n⏳ Waiting for memory indexing (3s)...")
|
print("\n⏳ Waiting for memory indexing (3s)...")
|
||||||
@ -270,7 +277,7 @@ async def main():
|
|||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
)
|
)
|
||||||
memories = result.get("result", {}).get("memories", [])
|
memories = result.get("result", {}).get("memories", [])
|
||||||
print(f" Q: \"{query}\"")
|
print(f' Q: "{query}"')
|
||||||
if memories:
|
if memories:
|
||||||
for mem in memories[:2]:
|
for mem in memories[:2]:
|
||||||
content = str(mem)[:100]
|
content = str(mem)[:100]
|
||||||
@ -281,7 +288,12 @@ async def main():
|
|||||||
|
|
||||||
# Relationship vector
|
# Relationship vector
|
||||||
print("📊 Relationship Vector Evolution:\n")
|
print("📊 Relationship Vector Evolution:\n")
|
||||||
prior = {"relationship_depth": 0.0, "emotional_valence": 0.0, "trust_level": 0.0, "pending_foresight": 0.0}
|
prior = {
|
||||||
|
"relationship_depth": 0.0,
|
||||||
|
"emotional_valence": 0.0,
|
||||||
|
"trust_level": 0.0,
|
||||||
|
"pending_foresight": 0.0,
|
||||||
|
}
|
||||||
deltas = [
|
deltas = [
|
||||||
{"relationship_depth": 0.1, "emotional_valence": 0.2, "trust_level": 0.05},
|
{"relationship_depth": 0.1, "emotional_valence": 0.2, "trust_level": 0.05},
|
||||||
{"relationship_depth": 0.05, "emotional_valence": 0.1, "trust_level": 0.1},
|
{"relationship_depth": 0.05, "emotional_valence": 0.1, "trust_level": 0.1},
|
||||||
@ -290,61 +302,147 @@ async def main():
|
|||||||
|
|
||||||
ema = None
|
ema = None
|
||||||
for i, delta in enumerate(deltas):
|
for i, delta in enumerate(deltas):
|
||||||
ema = apply_relationship_ema(prior, delta, conversation_depth=0.2 * (i + 1), prev_ema=ema)
|
ema = apply_relationship_ema(
|
||||||
print(f" Turn {i+1}: depth={ema['relationship_depth']:.3f} "
|
prior, delta, conversation_depth=0.2 * (i + 1), prev_ema=ema
|
||||||
f"valence={ema['emotional_valence']:.3f} "
|
)
|
||||||
f"trust={ema['trust_level']:.3f}")
|
print(
|
||||||
|
f" Turn {i + 1}: depth={ema['relationship_depth']:.3f} "
|
||||||
|
f"valence={ema['emotional_valence']:.3f} "
|
||||||
|
f"trust={ema['trust_level']:.3f}"
|
||||||
|
)
|
||||||
prior = ema
|
prior = ema
|
||||||
|
|
||||||
print(f"\n → After 3 turns: no longer a stranger (depth={ema['relationship_depth']:.3f})")
|
print(
|
||||||
print(f" → Neural network now produces warmer, more familiar behavioral signals\n")
|
"\n → After 3 turns: no longer a stranger "
|
||||||
|
f"(depth={ema['relationship_depth']:.3f})"
|
||||||
|
)
|
||||||
|
print(" → Neural network now produces warmer, more familiar behavioral signals\n")
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
print("✅ Demo complete!")
|
print("✅ Demo complete!")
|
||||||
|
|
||||||
|
|
||||||
async def demo_simulation():
|
async def demo_simulation():
|
||||||
"""Run demo in simulation mode (no EverCore connection)."""
|
"""Run demo in simulation mode (no EverOS connection)."""
|
||||||
print("📊 Simulating Relationship Vector Evolution:\n")
|
print("📊 Simulating Relationship Vector Evolution:\n")
|
||||||
print(" This shows how the 4D EverCore relationship vector")
|
print(" This shows how the 4D EverOS relationship vector")
|
||||||
print(" deepens over multiple conversation turns.\n")
|
print(" deepens over multiple conversation turns.\n")
|
||||||
|
|
||||||
prior = {"relationship_depth": 0.0, "emotional_valence": 0.0, "trust_level": 0.0, "pending_foresight": 0.0}
|
prior = {
|
||||||
|
"relationship_depth": 0.0,
|
||||||
|
"emotional_valence": 0.0,
|
||||||
|
"trust_level": 0.0,
|
||||||
|
"pending_foresight": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
# Simulate 10 turns of conversation
|
# Simulate 10 turns of conversation
|
||||||
simulated_deltas = [
|
simulated_deltas = [
|
||||||
(0.3, {"relationship_depth": 0.10, "emotional_valence": 0.15, "trust_level": 0.05}),
|
(
|
||||||
(0.4, {"relationship_depth": 0.08, "emotional_valence": 0.10, "trust_level": 0.08}),
|
0.3,
|
||||||
(0.5, {"relationship_depth": 0.05, "emotional_valence": 0.20, "trust_level": 0.12}),
|
{
|
||||||
(0.6, {"relationship_depth": 0.06, "emotional_valence": -0.10, "trust_level": 0.03}),
|
"relationship_depth": 0.10,
|
||||||
(0.7, {"relationship_depth": 0.04, "emotional_valence": 0.08, "trust_level": 0.10}),
|
"emotional_valence": 0.15,
|
||||||
(0.7, {"relationship_depth": 0.03, "emotional_valence": 0.12, "trust_level": 0.08}),
|
"trust_level": 0.05,
|
||||||
(0.8, {"relationship_depth": 0.02, "emotional_valence": 0.05, "trust_level": 0.06}),
|
},
|
||||||
(0.8, {"relationship_depth": 0.03, "emotional_valence": 0.10, "trust_level": 0.05}),
|
),
|
||||||
(0.9, {"relationship_depth": 0.01, "emotional_valence": 0.08, "trust_level": 0.04}),
|
(
|
||||||
(0.9, {"relationship_depth": 0.02, "emotional_valence": 0.06, "trust_level": 0.03}),
|
0.4,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.08,
|
||||||
|
"emotional_valence": 0.10,
|
||||||
|
"trust_level": 0.08,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.5,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.05,
|
||||||
|
"emotional_valence": 0.20,
|
||||||
|
"trust_level": 0.12,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.6,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.06,
|
||||||
|
"emotional_valence": -0.10,
|
||||||
|
"trust_level": 0.03,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.7,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.04,
|
||||||
|
"emotional_valence": 0.08,
|
||||||
|
"trust_level": 0.10,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.7,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.03,
|
||||||
|
"emotional_valence": 0.12,
|
||||||
|
"trust_level": 0.08,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.8,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.02,
|
||||||
|
"emotional_valence": 0.05,
|
||||||
|
"trust_level": 0.06,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.8,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.03,
|
||||||
|
"emotional_valence": 0.10,
|
||||||
|
"trust_level": 0.05,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.9,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.01,
|
||||||
|
"emotional_valence": 0.08,
|
||||||
|
"trust_level": 0.04,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0.9,
|
||||||
|
{
|
||||||
|
"relationship_depth": 0.02,
|
||||||
|
"emotional_valence": 0.06,
|
||||||
|
"trust_level": 0.03,
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
ema = None
|
ema = None
|
||||||
for i, (depth, delta) in enumerate(simulated_deltas, 1):
|
for i, (depth, delta) in enumerate(simulated_deltas, 1):
|
||||||
alpha = max(0.15, min(0.65, 0.15 + 0.5 * depth))
|
alpha = max(0.15, min(0.65, 0.15 + 0.5 * depth))
|
||||||
ema = apply_relationship_ema(prior, delta, conversation_depth=depth, prev_ema=ema)
|
ema = apply_relationship_ema(
|
||||||
|
prior, delta, conversation_depth=depth, prev_ema=ema
|
||||||
|
)
|
||||||
bar_d = "█" * int(ema["relationship_depth"] * 20)
|
bar_d = "█" * int(ema["relationship_depth"] * 20)
|
||||||
bar_v = "█" * int(max(0, ema["emotional_valence"]) * 20)
|
bar_v = "█" * int(max(0, ema["emotional_valence"]) * 20)
|
||||||
bar_t = "█" * int(ema["trust_level"] * 20)
|
bar_t = "█" * int(ema["trust_level"] * 20)
|
||||||
print(f" Turn {i:2d} (α={alpha:.2f}): "
|
print(
|
||||||
f"depth={ema['relationship_depth']:.3f} {bar_d}")
|
f" Turn {i:2d} (α={alpha:.2f}): "
|
||||||
print(f" "
|
f"depth={ema['relationship_depth']:.3f} {bar_d}"
|
||||||
f"valence={ema['emotional_valence']:+.3f} {bar_v}")
|
)
|
||||||
print(f" "
|
print(f" valence={ema['emotional_valence']:+.3f} {bar_v}")
|
||||||
f"trust={ema['trust_level']:.3f} {bar_t}")
|
print(f" trust={ema['trust_level']:.3f} {bar_t}")
|
||||||
print()
|
print()
|
||||||
prior = ema
|
prior = ema
|
||||||
|
|
||||||
print(" ──────────────────────────────────")
|
print(" ──────────────────────────────────")
|
||||||
print(f" Final state: depth={ema['relationship_depth']:.3f}, "
|
print(
|
||||||
f"valence={ema['emotional_valence']:+.3f}, "
|
f" Final state: depth={ema['relationship_depth']:.3f}, "
|
||||||
f"trust={ema['trust_level']:.3f}")
|
f"valence={ema['emotional_valence']:+.3f}, "
|
||||||
|
f"trust={ema['trust_level']:.3f}"
|
||||||
|
)
|
||||||
print()
|
print()
|
||||||
print(" Turn 4 shows a negative emotional event (valence delta = -0.10),")
|
print(" Turn 4 shows a negative emotional event (valence delta = -0.10),")
|
||||||
print(" but the EMA smoothing prevents overreaction — the relationship")
|
print(" but the EMA smoothing prevents overreaction — the relationship")
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Neural network context features — showing how EverCore expands
|
Neural network context features — showing how EverOS expands
|
||||||
the persona engine's perception from 8D to 12D.
|
the persona engine's perception from 8D to 12D.
|
||||||
|
|
||||||
The 4 additional relationship dimensions from EverCore allow the
|
The 4 additional relationship dimensions from EverOS allow the
|
||||||
neural network to produce different behavioral signals depending
|
neural network to produce different behavioral signals depending
|
||||||
on the history between user and persona.
|
on the history between user and persona.
|
||||||
|
|
||||||
@ -13,21 +13,21 @@ Full source: https://github.com/kellyvv/OpenHer/blob/main/engine/genome/genome_e
|
|||||||
# 5D Drive System (internal motivation)
|
# 5D Drive System (internal motivation)
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
|
|
||||||
DRIVES = ['connection', 'novelty', 'expression', 'safety', 'play']
|
DRIVES = ["connection", "novelty", "expression", "safety", "play"]
|
||||||
|
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
# 8D Behavioral Signals (neural network output)
|
# 8D Behavioral Signals (neural network output)
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
|
|
||||||
SIGNALS = [
|
SIGNALS = [
|
||||||
'directness', # 0=indirect hints → 1=straight talk
|
"directness", # 0=indirect hints → 1=straight talk
|
||||||
'vulnerability', # 0=guarded → 1=emotionally open
|
"vulnerability", # 0=guarded → 1=emotionally open
|
||||||
'playfulness', # 0=serious → 1=playful/teasing
|
"playfulness", # 0=serious → 1=playful/teasing
|
||||||
'initiative', # 0=reactive → 1=proactive leading
|
"initiative", # 0=reactive → 1=proactive leading
|
||||||
'depth', # 0=small talk → 1=deep conversation
|
"depth", # 0=small talk → 1=deep conversation
|
||||||
'warmth', # 0=cold/distant → 1=warm/caring
|
"warmth", # 0=cold/distant → 1=warm/caring
|
||||||
'defiance', # 0=compliant → 1=rebellious/stubborn
|
"defiance", # 0=compliant → 1=rebellious/stubborn
|
||||||
'curiosity', # 0=indifferent → 1=intensely curious
|
"curiosity", # 0=indifferent → 1=intensely curious
|
||||||
]
|
]
|
||||||
|
|
||||||
# ══════════════════════════════════════════════
|
# ══════════════════════════════════════════════
|
||||||
@ -36,31 +36,30 @@ SIGNALS = [
|
|||||||
|
|
||||||
CONTEXT_FEATURES = [
|
CONTEXT_FEATURES = [
|
||||||
# ── 8D from Critic LLM (per-turn perception) ──
|
# ── 8D from Critic LLM (per-turn perception) ──
|
||||||
'user_emotion', # -1=negative → 1=positive
|
"user_emotion", # -1=negative → 1=positive
|
||||||
'topic_intimacy', # 0=professional → 1=intimate
|
"topic_intimacy", # 0=professional → 1=intimate
|
||||||
'time_of_day', # 0=morning → 1=late night
|
"time_of_day", # 0=morning → 1=late night
|
||||||
'conversation_depth', # 0=just started → 1=deep conversation
|
"conversation_depth", # 0=just started → 1=deep conversation
|
||||||
'user_engagement', # 0=dismissive → 1=invested
|
"user_engagement", # 0=dismissive → 1=invested
|
||||||
'conflict_level', # 0=harmonious → 1=conflict
|
"conflict_level", # 0=harmonious → 1=conflict
|
||||||
'novelty_level', # 0=routine topic → 1=novel topic
|
"novelty_level", # 0=routine topic → 1=novel topic
|
||||||
'user_vulnerability', # 0=guarded → 1=open
|
"user_vulnerability", # 0=guarded → 1=open
|
||||||
|
# ── 4D from EverOS (cross-session relationship) ──
|
||||||
# ── 4D from EverCore (cross-session relationship) ──
|
"relationship_depth", # 0=stranger → 1=old friend
|
||||||
'relationship_depth', # 0=stranger → 1=old friend
|
"emotional_valence", # -1=negative history → 1=positive history
|
||||||
'emotional_valence', # -1=negative history → 1=positive history
|
"trust_level", # 0=no trust → 1=deep trust
|
||||||
'trust_level', # 0=no trust → 1=deep trust
|
"pending_foresight", # 0=nothing pending → 1=unresolved concern
|
||||||
'pending_foresight', # 0=nothing pending → 1=unresolved concern
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Neural network dimensions
|
# Neural network dimensions
|
||||||
N_DRIVES = len(DRIVES) # 5
|
N_DRIVES = len(DRIVES) # 5
|
||||||
N_CONTEXT = len(CONTEXT_FEATURES) # 12 (8 + 4 from EverCore)
|
N_CONTEXT = len(CONTEXT_FEATURES) # 12 (8 + 4 from EverOS)
|
||||||
N_SIGNALS = len(SIGNALS) # 8
|
N_SIGNALS = len(SIGNALS) # 8
|
||||||
RECURRENT_SIZE = 8 # Internal "mood" state
|
RECURRENT_SIZE = 8 # Internal "mood" state
|
||||||
INPUT_SIZE = N_DRIVES + N_CONTEXT + RECURRENT_SIZE # 5 + 12 + 8 = 25
|
INPUT_SIZE = N_DRIVES + N_CONTEXT + RECURRENT_SIZE # 5 + 12 + 8 = 25
|
||||||
HIDDEN_SIZE = 24
|
HIDDEN_SIZE = 24
|
||||||
|
|
||||||
# Architecture: 25D input → 24D hidden (tanh) → 8D output (sigmoid)
|
# Architecture: 25D input → 24D hidden (tanh) → 8D output (sigmoid)
|
||||||
# The 4 EverCore dimensions mean the same neural network produces
|
# The 4 EverOS dimensions mean the same neural network produces
|
||||||
# DIFFERENT behavioral signals for strangers vs. old friends,
|
# DIFFERENT behavioral signals for strangers vs. old friends,
|
||||||
# even with identical conversation context.
|
# even with identical conversation context.
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
EverMemosMixin — EverCore integration for ChatAgent.
|
EverMemosMixin — EverOS integration for ChatAgent.
|
||||||
|
|
||||||
This mixin handles all async memory operations in the ChatAgent lifecycle:
|
This mixin handles all async memory operations in the ChatAgent lifecycle:
|
||||||
Step 0: Session context loading (first turn)
|
Step 0: Session context loading (first turn)
|
||||||
Step 2.5: Relationship EMA (blend EverCore prior + LLM delta)
|
Step 2.5: Relationship EMA (blend EverOS prior + LLM delta)
|
||||||
Step 8.5: Collect async search results
|
Step 8.5: Collect async search results
|
||||||
Step 11: Fire-and-forget turn storage
|
Step 11: Fire-and-forget turn storage
|
||||||
Step 12: Async prefetch for next turn
|
Step 12: Async prefetch for next turn
|
||||||
|
|
||||||
The mixin pattern keeps EverCore concerns cleanly separated from the
|
The mixin pattern keeps EverOS concerns cleanly separated from the
|
||||||
core persona engine (drives, metabolism, neural network, style memory).
|
core persona engine (drives, metabolism, neural network, style memory).
|
||||||
|
|
||||||
Full source: https://github.com/kellyvv/OpenHer/blob/main/agent/evermemos_mixin.py
|
Full source: https://github.com/kellyvv/OpenHer/blob/main/agent/evermemos_mixin.py
|
||||||
@ -20,19 +20,19 @@ import asyncio
|
|||||||
|
|
||||||
|
|
||||||
class EverMemosMixin:
|
class EverMemosMixin:
|
||||||
"""EverCore async memory integration methods."""
|
"""EverOS async memory integration methods."""
|
||||||
|
|
||||||
async def _evermemos_gather(self) -> dict:
|
async def _evermemos_gather(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Step 0: Load EverCore session context (first turn only).
|
Step 0: Load EverOS session context (first turn only).
|
||||||
Subsequent turns reuse cached _session_ctx.
|
Subsequent turns reuse cached _session_ctx.
|
||||||
Returns relationship_4d dict for GenomeEngine context.
|
Returns relationship_4d dict for GenomeEngine context.
|
||||||
"""
|
"""
|
||||||
empty_4d = {
|
empty_4d = {
|
||||||
'relationship_depth': 0.0,
|
"relationship_depth": 0.0,
|
||||||
'emotional_valence': 0.0,
|
"emotional_valence": 0.0,
|
||||||
'trust_level': 0.0,
|
"trust_level": 0.0,
|
||||||
'pending_foresight': 0.0,
|
"pending_foresight": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not (self.evermemos and self.evermemos.available):
|
if not (self.evermemos and self.evermemos.available):
|
||||||
@ -75,10 +75,10 @@ class EverMemosMixin:
|
|||||||
"""
|
"""
|
||||||
# Map Critic output keys → context feature keys
|
# Map Critic output keys → context feature keys
|
||||||
delta_map = {
|
delta_map = {
|
||||||
'relationship_depth': rel_delta.get('relationship_delta', 0.0),
|
"relationship_depth": rel_delta.get("relationship_delta", 0.0),
|
||||||
'emotional_valence': rel_delta.get('emotional_valence', 0.0),
|
"emotional_valence": rel_delta.get("emotional_valence", 0.0),
|
||||||
'trust_level': rel_delta.get('trust_delta', 0.0),
|
"trust_level": rel_delta.get("trust_delta", 0.0),
|
||||||
'pending_foresight': 0.0, # No delta for foresight (data-driven only)
|
"pending_foresight": 0.0, # No delta for foresight (data-driven only)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize EMA on first turn
|
# Initialize EMA on first turn
|
||||||
@ -88,7 +88,7 @@ class EverMemosMixin:
|
|||||||
# Compute posterior = clip(prior + delta)
|
# Compute posterior = clip(prior + delta)
|
||||||
posterior = {}
|
posterior = {}
|
||||||
for k in prior:
|
for k in prior:
|
||||||
lo = -1.0 if k == 'emotional_valence' else 0.0
|
lo = -1.0 if k == "emotional_valence" else 0.0
|
||||||
posterior[k] = max(lo, min(1.0, prior[k] + delta_map.get(k, 0.0)))
|
posterior[k] = max(lo, min(1.0, prior[k] + delta_map.get(k, 0.0)))
|
||||||
|
|
||||||
# Depth-modulated alpha: shallow → trust prior, deep → trust LLM
|
# Depth-modulated alpha: shallow → trust prior, deep → trust LLM
|
||||||
@ -104,9 +104,10 @@ class EverMemosMixin:
|
|||||||
return ema
|
return ema
|
||||||
|
|
||||||
def _evermemos_store_bg(self, user_message: str, reply: str) -> None:
|
def _evermemos_store_bg(self, user_message: str, reply: str) -> None:
|
||||||
"""Step 11: Fire-and-forget EverCore storage (asyncio.create_task)."""
|
"""Step 11: Fire-and-forget EverOS storage (asyncio.create_task)."""
|
||||||
if not (self.evermemos and self.evermemos.available):
|
if not (self.evermemos and self.evermemos.available):
|
||||||
return
|
return
|
||||||
|
|
||||||
async def _do_store():
|
async def _do_store():
|
||||||
try:
|
try:
|
||||||
await self.evermemos.store_turn(
|
await self.evermemos.store_turn(
|
||||||
@ -120,6 +121,7 @@ class EverMemosMixin:
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [evermemos] ❌ store failed: {type(e).__name__}: {e}")
|
print(f" [evermemos] ❌ store failed: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(_do_store())
|
asyncio.create_task(_do_store())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -180,7 +182,7 @@ class EverMemosMixin:
|
|||||||
self._relevant_facts = facts
|
self._relevant_facts = facts
|
||||||
self._relevant_episodes = episodes
|
self._relevant_episodes = episodes
|
||||||
self._relevant_profile = profile
|
self._relevant_profile = profile
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
# Graceful degradation: use static session context
|
# Graceful degradation: use static session context
|
||||||
self._relevant_facts = ""
|
self._relevant_facts = ""
|
||||||
self._relevant_episodes = ""
|
self._relevant_episodes = ""
|
||||||
|
|||||||
@ -3,9 +3,9 @@ Memory shared types for OpenHer.
|
|||||||
|
|
||||||
These types bridge the two memory providers:
|
These types bridge the two memory providers:
|
||||||
- SoulMem (behavioral memory, always-on SQLite layer)
|
- SoulMem (behavioral memory, always-on SQLite layer)
|
||||||
- EverCore (declarative memory, cross-session persistence)
|
- EverOS (declarative memory, cross-session persistence)
|
||||||
|
|
||||||
The SessionContext is the key data structure loaded from EverCore
|
The SessionContext is the key data structure loaded from EverOS
|
||||||
at session start — it provides relationship priors, user profile,
|
at session start — it provides relationship priors, user profile,
|
||||||
episode summaries, and foresight data that expand the neural
|
episode summaries, and foresight data that expand the neural
|
||||||
network's perception from 8D to 12D.
|
network's perception from 8D to 12D.
|
||||||
@ -16,12 +16,12 @@ Full source: https://github.com/kellyvv/OpenHer/blob/main/memory/types.py
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Memory:
|
class Memory:
|
||||||
"""A single memory entry (SoulMem behavioral layer)."""
|
"""A single memory entry (SoulMem behavioral layer)."""
|
||||||
|
|
||||||
memory_id: int = 0
|
memory_id: int = 0
|
||||||
user_id: str = ""
|
user_id: str = ""
|
||||||
persona_id: str = ""
|
persona_id: str = ""
|
||||||
@ -35,7 +35,7 @@ class Memory:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SessionContext:
|
class SessionContext:
|
||||||
"""
|
"""
|
||||||
EverCore session context (declarative memory).
|
EverOS session context (declarative memory).
|
||||||
|
|
||||||
Loaded once at session start, this contains everything the
|
Loaded once at session start, this contains everything the
|
||||||
persona needs to know about the user from past sessions:
|
persona needs to know about the user from past sessions:
|
||||||
@ -53,6 +53,7 @@ class SessionContext:
|
|||||||
- Step 5: 4D vector enters neural network as context features
|
- Step 5: 4D vector enters neural network as context features
|
||||||
- Step 8.5: Used as fallback when async search times out
|
- Step 8.5: Used as fallback when async search times out
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_id: str = ""
|
user_id: str = ""
|
||||||
persona_id: str = ""
|
persona_id: str = ""
|
||||||
user_profile: str = ""
|
user_profile: str = ""
|
||||||
@ -63,4 +64,4 @@ class SessionContext:
|
|||||||
trust_level: float = 0.0
|
trust_level: float = 0.0
|
||||||
pending_foresight: float = 0.0
|
pending_foresight: float = 0.0
|
||||||
has_history: bool = False
|
has_history: bool = False
|
||||||
raw_data: Optional[dict] = None
|
raw_data: dict | None = None
|
||||||
|
|||||||
Reference in New Issue
Block a user