hermes-next 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hermes_next-0.1.0/.github/workflows/ci.yml +64 -0
- hermes_next-0.1.0/.github/workflows/publish.yml +31 -0
- hermes_next-0.1.0/.gitignore +33 -0
- hermes_next-0.1.0/LICENSE +17 -0
- hermes_next-0.1.0/PKG-INFO +118 -0
- hermes_next-0.1.0/README.md +91 -0
- hermes_next-0.1.0/hermes_next/__init__.py +11 -0
- hermes_next-0.1.0/hermes_next/cache/__init__.py +1 -0
- hermes_next-0.1.0/hermes_next/cache/connection.py +100 -0
- hermes_next-0.1.0/hermes_next/cache/policies.py +94 -0
- hermes_next-0.1.0/hermes_next/cache/schema.py +100 -0
- hermes_next-0.1.0/hermes_next/cache/skills.py +89 -0
- hermes_next-0.1.0/hermes_next/cache/traces.py +160 -0
- hermes_next-0.1.0/hermes_next/cache/vector.py +58 -0
- hermes_next-0.1.0/hermes_next/config.py +152 -0
- hermes_next-0.1.0/hermes_next/memos/__init__.py +1 -0
- hermes_next-0.1.0/hermes_next/memos/capture.py +76 -0
- hermes_next-0.1.0/hermes_next/memos/id.py +79 -0
- hermes_next-0.1.0/hermes_next/memos/pipeline.py +281 -0
- hermes_next-0.1.0/hermes_next/memos/policy.py +394 -0
- hermes_next-0.1.0/hermes_next/memos/retrieval.py +90 -0
- hermes_next-0.1.0/hermes_next/memos/reward.py +236 -0
- hermes_next-0.1.0/hermes_next/memos/skill.py +219 -0
- hermes_next-0.1.0/hermes_next/memos/types.py +148 -0
- hermes_next-0.1.0/hermes_next/memos/world_model.py +466 -0
- hermes_next-0.1.0/hermes_next/ov/__init__.py +1 -0
- hermes_next-0.1.0/hermes_next/ov/client.py +175 -0
- hermes_next-0.1.0/hermes_next/ov/session.py +101 -0
- hermes_next-0.1.0/hermes_next/provider.py +325 -0
- hermes_next-0.1.0/hermes_next/retrieval/__init__.py +1 -0
- hermes_next-0.1.0/hermes_next/retrieval/pipeline.py +154 -0
- hermes_next-0.1.0/hermes_next/retrieval/ranker.py +112 -0
- hermes_next-0.1.0/hermes_next/viewer/__init__.py +5 -0
- hermes_next-0.1.0/hermes_next/viewer/server.py +637 -0
- hermes_next-0.1.0/plugin.yaml +7 -0
- hermes_next-0.1.0/pyproject.toml +69 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- name: Install ruff
|
|
18
|
+
run: pip install ruff
|
|
19
|
+
- name: Lint
|
|
20
|
+
run: ruff check hermes_next/ tests/
|
|
21
|
+
- name: Format check
|
|
22
|
+
run: ruff format --check hermes_next/ tests/ || true
|
|
23
|
+
|
|
24
|
+
type-check:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.12"
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: |
|
|
33
|
+
pip install mypy
|
|
34
|
+
pip install -e "."
|
|
35
|
+
- name: Type check
|
|
36
|
+
run: mypy hermes_next/ --ignore-missing-imports --follow-imports=skip || true
|
|
37
|
+
|
|
38
|
+
test:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
strategy:
|
|
41
|
+
matrix:
|
|
42
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
43
|
+
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
47
|
+
uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: ${{ matrix.python-version }}
|
|
50
|
+
|
|
51
|
+
- name: Install uv
|
|
52
|
+
run: |
|
|
53
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
54
|
+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
55
|
+
|
|
56
|
+
- name: Install package
|
|
57
|
+
run: |
|
|
58
|
+
uv pip install --system -e "."
|
|
59
|
+
uv pip install --system pytest pytest-asyncio httpx numpy
|
|
60
|
+
|
|
61
|
+
- name: Test
|
|
62
|
+
run: pytest tests/ -v -k "not integration" --timeout=30
|
|
63
|
+
env:
|
|
64
|
+
HERMES_NEXT_OV_URL: ${{ secrets.HERMES_NEXT_OV_URL || 'http://localhost:1933' }}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: release
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install build tools
|
|
21
|
+
run: pip install hatchling
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: |
|
|
25
|
+
python -m hatchling build
|
|
26
|
+
ls -la dist/
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
skip-existing: true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
|
|
8
|
+
# Cache
|
|
9
|
+
*.db
|
|
10
|
+
*.db-wal
|
|
11
|
+
*.db-shm
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.vscode/
|
|
15
|
+
.idea/
|
|
16
|
+
*.swp
|
|
17
|
+
*.swo
|
|
18
|
+
|
|
19
|
+
# Environment
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# Test artifacts
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
|
|
32
|
+
# Project
|
|
33
|
+
*.lock
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2024 jigeagent
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-next
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Next-generation memory provider for Hermes Agent — fusing OpenViking vector storage with MemOS cognitive engine
|
|
5
|
+
Project-URL: Homepage, https://github.com/jigeagent/hermes-next
|
|
6
|
+
Project-URL: Repository, https://github.com/jigeagent/hermes-next.git
|
|
7
|
+
Project-URL: Documentation, https://github.com/jigeagent/hermes-next#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/jigeagent/hermes-next/issues
|
|
9
|
+
Author-email: jigeagent <oss@jigeagent.ai>
|
|
10
|
+
License: AGPL-3.0-or-later
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cognitive,hermes-agent,memory,memos,openviking,rag,vector-search
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: openviking>=0.3.22
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Hermes Next
|
|
29
|
+
|
|
30
|
+
**Next-generation memory provider for Hermes Agent.**
|
|
31
|
+
|
|
32
|
+
Hermes Next fuses [OpenViking](https://github.com/bytedance/openviking) vector storage with a Python-native [MemOS](https://github.com/memtensor/memos) cognitive engine, giving Hermes Agent agents persistent,结构化记忆能力。
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **OpenViking Backend** — Long-term vector storage, semantic retrieval, session management
|
|
37
|
+
- **MemOS Cognitive Pipeline** — L1 Trace capture → reward backpropagation → L2 Policy induction → L3 World Model → Skill crystallization
|
|
38
|
+
- **Python Native** — Zero bridging overhead, runs in-process
|
|
39
|
+
- **Local SQLite Cache** — FTS5 full-text search + numpy-based cosine similarity, zero dependencies beyond stdlib
|
|
40
|
+
- **Fusion Retrieval** — 6-step pipeline combining semantic search, full-text search, policy matching, timeline, recency boost, and MMR diversification
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install hermes-next
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10+ and a running OpenViking server (v0.3.22+).
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Create a `hermes-next.yaml` in your config directory:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
openviking:
|
|
56
|
+
base_url: "http://localhost:1933"
|
|
57
|
+
api_key: null
|
|
58
|
+
|
|
59
|
+
cache:
|
|
60
|
+
path: "~/.hermes-next/cache.db"
|
|
61
|
+
enable_fts: true
|
|
62
|
+
|
|
63
|
+
agent:
|
|
64
|
+
name: "default"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or set environment variables:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export HERMES_NEXT_OV_URL="http://localhost:1933"
|
|
71
|
+
export HERMES_NEXT_CACHE_PATH="~/.hermes-next/cache.db"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage with Hermes Agent
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from hermes_next import HermesNextProvider
|
|
78
|
+
|
|
79
|
+
provider = HermesNextProvider()
|
|
80
|
+
provider.initialize(session_id="my-session")
|
|
81
|
+
|
|
82
|
+
# The provider handles prefetching, storage, and retrieval automatically
|
|
83
|
+
context = provider.prefetch("What did we discuss about RAG?")
|
|
84
|
+
print(context)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or via CLI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
hermes agent --memory-provider hermes-next
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Tools
|
|
94
|
+
|
|
95
|
+
The provider exposes these tools to the agent:
|
|
96
|
+
|
|
97
|
+
| Tool | Description |
|
|
98
|
+
|------|-------------|
|
|
99
|
+
| `memos_search(query, k)` | Semantic search across traces |
|
|
100
|
+
| `memos_get(trace_id)` | Read a specific trace |
|
|
101
|
+
| `memos_timeline(limit)` | Recent activity timeline |
|
|
102
|
+
|
|
103
|
+
## Project Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
hermes-next/
|
|
107
|
+
├── hermes_next/
|
|
108
|
+
│ ├── ov/ # OpenViking REST client
|
|
109
|
+
│ ├── memos/ # MemOS cognitive engine
|
|
110
|
+
│ ├── cache/ # SQLite local cache
|
|
111
|
+
│ └── retrieval/ # Fusion retrieval pipeline
|
|
112
|
+
├── tests/
|
|
113
|
+
└── plugin.yaml # Hermes plugin manifest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
AGPL-3.0 — This project is a derivative of OpenViking (AGPL-3.0).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Hermes Next
|
|
2
|
+
|
|
3
|
+
**Next-generation memory provider for Hermes Agent.**
|
|
4
|
+
|
|
5
|
+
Hermes Next fuses [OpenViking](https://github.com/bytedance/openviking) vector storage with a Python-native [MemOS](https://github.com/memtensor/memos) cognitive engine, giving Hermes Agent agents persistent,结构化记忆能力。
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **OpenViking Backend** — Long-term vector storage, semantic retrieval, session management
|
|
10
|
+
- **MemOS Cognitive Pipeline** — L1 Trace capture → reward backpropagation → L2 Policy induction → L3 World Model → Skill crystallization
|
|
11
|
+
- **Python Native** — Zero bridging overhead, runs in-process
|
|
12
|
+
- **Local SQLite Cache** — FTS5 full-text search + numpy-based cosine similarity, zero dependencies beyond stdlib
|
|
13
|
+
- **Fusion Retrieval** — 6-step pipeline combining semantic search, full-text search, policy matching, timeline, recency boost, and MMR diversification
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install hermes-next
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.10+ and a running OpenViking server (v0.3.22+).
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Create a `hermes-next.yaml` in your config directory:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
openviking:
|
|
29
|
+
base_url: "http://localhost:1933"
|
|
30
|
+
api_key: null
|
|
31
|
+
|
|
32
|
+
cache:
|
|
33
|
+
path: "~/.hermes-next/cache.db"
|
|
34
|
+
enable_fts: true
|
|
35
|
+
|
|
36
|
+
agent:
|
|
37
|
+
name: "default"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or set environment variables:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export HERMES_NEXT_OV_URL="http://localhost:1933"
|
|
44
|
+
export HERMES_NEXT_CACHE_PATH="~/.hermes-next/cache.db"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage with Hermes Agent
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from hermes_next import HermesNextProvider
|
|
51
|
+
|
|
52
|
+
provider = HermesNextProvider()
|
|
53
|
+
provider.initialize(session_id="my-session")
|
|
54
|
+
|
|
55
|
+
# The provider handles prefetching, storage, and retrieval automatically
|
|
56
|
+
context = provider.prefetch("What did we discuss about RAG?")
|
|
57
|
+
print(context)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or via CLI:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
hermes agent --memory-provider hermes-next
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Tools
|
|
67
|
+
|
|
68
|
+
The provider exposes these tools to the agent:
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
|------|-------------|
|
|
72
|
+
| `memos_search(query, k)` | Semantic search across traces |
|
|
73
|
+
| `memos_get(trace_id)` | Read a specific trace |
|
|
74
|
+
| `memos_timeline(limit)` | Recent activity timeline |
|
|
75
|
+
|
|
76
|
+
## Project Structure
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
hermes-next/
|
|
80
|
+
├── hermes_next/
|
|
81
|
+
│ ├── ov/ # OpenViking REST client
|
|
82
|
+
│ ├── memos/ # MemOS cognitive engine
|
|
83
|
+
│ ├── cache/ # SQLite local cache
|
|
84
|
+
│ └── retrieval/ # Fusion retrieval pipeline
|
|
85
|
+
├── tests/
|
|
86
|
+
└── plugin.yaml # Hermes plugin manifest
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
AGPL-3.0 — This project is a derivative of OpenViking (AGPL-3.0).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Hermes Next — Next-generation memory provider for Hermes Agent."""
|
|
2
|
+
|
|
3
|
+
from hermes_next.provider import HermesNextProvider
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = ["HermesNextProvider", "register"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register():
|
|
10
|
+
"""Plugin entry point — returns the provider class for Hermes Agent discovery."""
|
|
11
|
+
return HermesNextProvider
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SQLite local cache layer."""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""SQLite connection management with performance optimizations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Shared statement cache across threads
|
|
12
|
+
_STMT_CACHE: dict[str, sqlite3.Cursor] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheConnection:
|
|
16
|
+
"""Thread-safe SQLite connection manager with performance tuning.
|
|
17
|
+
|
|
18
|
+
Optimizations:
|
|
19
|
+
- WAL mode for concurrent reads
|
|
20
|
+
- 64MB cache for hot data
|
|
21
|
+
- memory-mapped I/O (256MB)
|
|
22
|
+
- Lazy pragma initialization (deferred until first query)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, db_path: str, wal_mode: bool = True):
|
|
26
|
+
self._db_path = str(Path(db_path).expanduser())
|
|
27
|
+
self._wal = wal_mode
|
|
28
|
+
self._local = threading.local()
|
|
29
|
+
self._lock = threading.Lock()
|
|
30
|
+
self._initialized = False
|
|
31
|
+
|
|
32
|
+
# Ensure parent directory exists
|
|
33
|
+
Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
def _ensure_init(self) -> None:
|
|
36
|
+
"""Apply performance pragmas once per connection."""
|
|
37
|
+
if self._initialized:
|
|
38
|
+
return
|
|
39
|
+
conn = self._get_conn_raw()
|
|
40
|
+
if self._wal:
|
|
41
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
42
|
+
conn.executescript("""
|
|
43
|
+
PRAGMA synchronous=NORMAL;
|
|
44
|
+
PRAGMA foreign_keys=ON;
|
|
45
|
+
PRAGMA cache_size=-65536;
|
|
46
|
+
PRAGMA mmap_size=268435456;
|
|
47
|
+
PRAGMA temp_store=MEMORY;
|
|
48
|
+
PRAGMA busy_timeout=5000;
|
|
49
|
+
""")
|
|
50
|
+
self._initialized = True
|
|
51
|
+
|
|
52
|
+
def _get_conn_raw(self) -> sqlite3.Connection:
|
|
53
|
+
"""Create a raw connection without pragma setup."""
|
|
54
|
+
if not hasattr(self._local, "conn") or self._local.conn is None:
|
|
55
|
+
conn = sqlite3.connect(
|
|
56
|
+
self._db_path,
|
|
57
|
+
check_same_thread=False,
|
|
58
|
+
isolation_level=None, # autocommit mode
|
|
59
|
+
)
|
|
60
|
+
conn.row_factory = sqlite3.Row
|
|
61
|
+
self._local.conn = conn
|
|
62
|
+
return self._local.conn
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def conn(self) -> sqlite3.Connection:
|
|
66
|
+
self._ensure_init()
|
|
67
|
+
return self._get_conn_raw()
|
|
68
|
+
|
|
69
|
+
def execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
|
|
70
|
+
"""Execute with automatic pragma init."""
|
|
71
|
+
self._ensure_init()
|
|
72
|
+
return self._get_conn_raw().execute(sql, params)
|
|
73
|
+
|
|
74
|
+
def executemany(self, sql: str, params: list[tuple]) -> sqlite3.Cursor:
|
|
75
|
+
"""Batch execute with automatic pragma init."""
|
|
76
|
+
self._ensure_init()
|
|
77
|
+
return self._get_conn_raw().executemany(sql, params)
|
|
78
|
+
|
|
79
|
+
def close(self) -> None:
|
|
80
|
+
"""Close the connection for the current thread."""
|
|
81
|
+
if hasattr(self._local, "conn") and self._local.conn is not None:
|
|
82
|
+
try:
|
|
83
|
+
self._local.conn.execute("PRAGMA optimize")
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
self._local.conn.close()
|
|
87
|
+
self._local.conn = None
|
|
88
|
+
self._initialized = False
|
|
89
|
+
|
|
90
|
+
def close_all(self) -> None:
|
|
91
|
+
"""Force close via lock (use sparingly)."""
|
|
92
|
+
with self._lock:
|
|
93
|
+
if hasattr(self._local, "conn") and self._local.conn is not None:
|
|
94
|
+
try:
|
|
95
|
+
self._local.conn.execute("PRAGMA optimize")
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
self._local.conn.close()
|
|
99
|
+
self._local.conn = None
|
|
100
|
+
self._initialized = False
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Policy repository — local SQLite CRUD for policies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from hermes_next.cache.connection import CacheConnection
|
|
9
|
+
from hermes_next.cache.schema import ensure_schema
|
|
10
|
+
from hermes_next.memos.types import PolicyRow
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PolicyRepository:
|
|
14
|
+
"""Persist and query policies locally."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, cache: CacheConnection):
|
|
17
|
+
self._cache = cache
|
|
18
|
+
ensure_schema(cache)
|
|
19
|
+
|
|
20
|
+
def insert(self, policy: PolicyRow) -> None:
|
|
21
|
+
"""Insert a policy into local cache."""
|
|
22
|
+
conn = self._cache.conn
|
|
23
|
+
conn.execute(
|
|
24
|
+
"""
|
|
25
|
+
INSERT OR REPLACE INTO policies
|
|
26
|
+
(id, name, description, trigger_pattern, action_template,
|
|
27
|
+
embedding, confidence, activation_count, source_trace_ids,
|
|
28
|
+
metadata, created_at, synced)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
30
|
+
""",
|
|
31
|
+
(
|
|
32
|
+
policy.id,
|
|
33
|
+
policy.name,
|
|
34
|
+
policy.description,
|
|
35
|
+
policy.trigger_pattern,
|
|
36
|
+
policy.action_template,
|
|
37
|
+
json.dumps(policy.embedding) if policy.embedding else None,
|
|
38
|
+
policy.confidence,
|
|
39
|
+
policy.activation_count,
|
|
40
|
+
json.dumps(policy.source_trace_ids, ensure_ascii=False),
|
|
41
|
+
json.dumps(policy.metadata, ensure_ascii=False, default=str),
|
|
42
|
+
policy.created_at,
|
|
43
|
+
0,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
conn.commit()
|
|
47
|
+
|
|
48
|
+
def get(self, policy_id: str) -> Optional[PolicyRow]:
|
|
49
|
+
"""Get a policy by ID."""
|
|
50
|
+
row = self._cache.conn.execute(
|
|
51
|
+
"SELECT * FROM policies WHERE id = ?", (policy_id,)
|
|
52
|
+
).fetchone()
|
|
53
|
+
if row is None:
|
|
54
|
+
return None
|
|
55
|
+
return self._row_to_policy(row)
|
|
56
|
+
|
|
57
|
+
def list_active(self, min_confidence: float = 0.3, limit: int = 20) -> list[PolicyRow]:
|
|
58
|
+
"""List policies with confidence above threshold."""
|
|
59
|
+
rows = self._cache.conn.execute(
|
|
60
|
+
"SELECT * FROM policies WHERE confidence >= ? ORDER BY confidence DESC LIMIT ?",
|
|
61
|
+
(min_confidence, limit),
|
|
62
|
+
).fetchall()
|
|
63
|
+
return [self._row_to_policy(r) for r in rows]
|
|
64
|
+
|
|
65
|
+
def increment_activation(self, policy_id: str) -> None:
|
|
66
|
+
"""Increment activation count for a policy."""
|
|
67
|
+
self._cache.conn.execute(
|
|
68
|
+
"UPDATE policies SET activation_count = activation_count + 1 WHERE id = ?",
|
|
69
|
+
(policy_id,),
|
|
70
|
+
)
|
|
71
|
+
self._cache.conn.commit()
|
|
72
|
+
|
|
73
|
+
def count(self) -> int:
|
|
74
|
+
"""Total policy count."""
|
|
75
|
+
row = self._cache.conn.execute("SELECT COUNT(*) as cnt FROM policies").fetchone()
|
|
76
|
+
return row["cnt"] if row else 0
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _row_to_policy(row: Any) -> PolicyRow:
|
|
80
|
+
return PolicyRow(
|
|
81
|
+
id=row["id"],
|
|
82
|
+
name=row["name"],
|
|
83
|
+
description=row["description"],
|
|
84
|
+
trigger_pattern=row["trigger_pattern"],
|
|
85
|
+
action_template=row["action_template"],
|
|
86
|
+
embedding=json.loads(row["embedding"]) if row["embedding"] else None,
|
|
87
|
+
confidence=row["confidence"],
|
|
88
|
+
activation_count=row["activation_count"],
|
|
89
|
+
source_trace_ids=json.loads(row["source_trace_ids"])
|
|
90
|
+
if isinstance(row["source_trace_ids"], str)
|
|
91
|
+
else [],
|
|
92
|
+
metadata=json.loads(row["metadata"]) if isinstance(row["metadata"], str) else {},
|
|
93
|
+
created_at=row["created_at"],
|
|
94
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""SQLite schema — traces, policies, skills tables with FTS5."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hermes_next.cache.connection import CacheConnection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ensure_schema(conn_or_cache: CacheConnection) -> None:
|
|
9
|
+
"""Create all tables and indexes if they don't exist."""
|
|
10
|
+
conn = conn_or_cache.conn if isinstance(conn_or_cache, CacheConnection) else conn_or_cache
|
|
11
|
+
|
|
12
|
+
conn.executescript("""
|
|
13
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
session_id TEXT NOT NULL,
|
|
16
|
+
turn_index INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
user_content TEXT NOT NULL,
|
|
18
|
+
assistant_content TEXT NOT NULL DEFAULT '',
|
|
19
|
+
embedding BLOB,
|
|
20
|
+
reward REAL NOT NULL DEFAULT 0.0,
|
|
21
|
+
tags TEXT DEFAULT '',
|
|
22
|
+
metadata TEXT DEFAULT '{}',
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
synced INTEGER NOT NULL DEFAULT 0
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_traces_session
|
|
28
|
+
ON traces(session_id);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_traces_created
|
|
30
|
+
ON traces(created_at);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_traces_synced
|
|
32
|
+
ON traces(synced);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
description TEXT NOT NULL DEFAULT '',
|
|
38
|
+
trigger_pattern TEXT NOT NULL DEFAULT '',
|
|
39
|
+
action_template TEXT NOT NULL DEFAULT '',
|
|
40
|
+
embedding BLOB,
|
|
41
|
+
confidence REAL NOT NULL DEFAULT 0.0,
|
|
42
|
+
activation_count INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
source_trace_ids TEXT DEFAULT '[]',
|
|
44
|
+
metadata TEXT DEFAULT '{}',
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
synced INTEGER NOT NULL DEFAULT 0
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_policies_confidence
|
|
50
|
+
ON policies(confidence DESC);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
53
|
+
name TEXT PRIMARY KEY,
|
|
54
|
+
description TEXT NOT NULL DEFAULT '',
|
|
55
|
+
usage_guide TEXT NOT NULL DEFAULT '',
|
|
56
|
+
source_policy_ids TEXT DEFAULT '[]',
|
|
57
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
58
|
+
metadata TEXT DEFAULT '{}',
|
|
59
|
+
created_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS traces_fts
|
|
63
|
+
USING fts5(
|
|
64
|
+
user_content,
|
|
65
|
+
assistant_content,
|
|
66
|
+
tags,
|
|
67
|
+
content='traces',
|
|
68
|
+
content_rowid='rowid'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TRIGGER IF NOT EXISTS traces_ai AFTER INSERT ON traces BEGIN
|
|
72
|
+
INSERT INTO traces_fts(rowid, user_content, assistant_content, tags)
|
|
73
|
+
VALUES (new.rowid, new.user_content, new.assistant_content, new.tags);
|
|
74
|
+
END;
|
|
75
|
+
|
|
76
|
+
CREATE TRIGGER IF NOT EXISTS traces_ad AFTER DELETE ON traces BEGIN
|
|
77
|
+
INSERT INTO traces_fts(traces_fts, rowid, user_content, assistant_content, tags)
|
|
78
|
+
VALUES ('delete', old.rowid, old.user_content, old.assistant_content, old.tags);
|
|
79
|
+
END;
|
|
80
|
+
|
|
81
|
+
CREATE TRIGGER IF NOT EXISTS traces_au AFTER UPDATE ON traces BEGIN
|
|
82
|
+
INSERT INTO traces_fts(traces_fts, rowid, user_content, assistant_content, tags)
|
|
83
|
+
VALUES ('delete', old.rowid, old.user_content, old.assistant_content, old.tags);
|
|
84
|
+
INSERT INTO traces_fts(rowid, user_content, assistant_content, tags)
|
|
85
|
+
VALUES (new.rowid, new.user_content, new.assistant_content, new.tags);
|
|
86
|
+
END;
|
|
87
|
+
""")
|
|
88
|
+
conn.commit()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def drop_schema(conn_or_cache: CacheConnection) -> None:
|
|
92
|
+
"""Drop all tables (for testing)."""
|
|
93
|
+
conn = conn_or_cache.conn if isinstance(conn_or_cache, CacheConnection) else conn_or_cache
|
|
94
|
+
conn.executescript("""
|
|
95
|
+
DROP TABLE IF EXISTS traces_fts;
|
|
96
|
+
DROP TABLE IF EXISTS skills;
|
|
97
|
+
DROP TABLE IF EXISTS policies;
|
|
98
|
+
DROP TABLE IF EXISTS traces;
|
|
99
|
+
""")
|
|
100
|
+
conn.commit()
|