Compare commits
9 Commits
7451cd73c9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 696fa3a1b8 | |||
| 1eb455d5b6 | |||
| 2b7495aa7d | |||
| 5f9294bdd8 | |||
|
|
acc42c4381 | ||
|
|
f410df3e7a | ||
|
|
bb1b1dad2f | ||
| 7e5bbabb29 | |||
| a5e49573ca |
21
MEMORY.md
21
MEMORY.md
@@ -103,6 +103,23 @@ _这份文件记录持续性项目和重要状态,跨会话保留。_
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 4. Notesearch 笔记搜索
|
||||||
|
**状态**: 运行中
|
||||||
|
**创建**: 2026-04-03
|
||||||
|
**配置**:
|
||||||
|
- 工具: `~/.openclaw/workspace/skills/notesearch/`
|
||||||
|
- 笔记库: `/home/lyx/Documents/obsidian-yanxin`(Obsidian vault,独立 git 仓库)
|
||||||
|
- 嵌入模型: `qwen3-embedding:0.6b`(通过 Ollama)
|
||||||
|
- 索引: `<vault>/.index/`(gitignored)
|
||||||
|
- 技术栈: LlamaIndex + Ollama
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
- 基于向量搜索的语义检索,用户提问时搜索 Obsidian 笔记
|
||||||
|
- 返回相关片段、文件路径和相关性分数
|
||||||
|
- 笔记更新后需重新索引(`notesearch.sh index`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 项目文件索引
|
## 📁 项目文件索引
|
||||||
|
|
||||||
| 项目 | 位置 |
|
| 项目 | 位置 |
|
||||||
@@ -112,7 +129,9 @@ _这份文件记录持续性项目和重要状态,跨会话保留。_
|
|||||||
| 日历/待办 | `~/.openclaw/workspace/skills/calendar/` |
|
| 日历/待办 | `~/.openclaw/workspace/skills/calendar/` |
|
||||||
| 日历数据 | `~/.openclaw/workspace/calendars/` (home=事件, tasks=待办) |
|
| 日历数据 | `~/.openclaw/workspace/calendars/` (home=事件, tasks=待办) |
|
||||||
| himalaya 包装器 | `~/.openclaw/workspace/scripts/himalaya.sh` |
|
| himalaya 包装器 | `~/.openclaw/workspace/scripts/himalaya.sh` |
|
||||||
|
| 笔记搜索 | `~/.openclaw/workspace/skills/notesearch/` |
|
||||||
|
| Obsidian 笔记库 | `/home/lyx/Documents/obsidian-yanxin` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_最后更新: 2026-03-31_
|
_最后更新: 2026-04-03_
|
||||||
|
|||||||
29
TOOLS.md
29
TOOLS.md
@@ -184,6 +184,35 @@ $SKILL_DIR/scripts/calendar.sh todo check # 每日摘要(cron)
|
|||||||
- **取消周期性事件的单次用 `--date`**,不要用 `--all`(会删掉整个系列)
|
- **取消周期性事件的单次用 `--date`**,不要用 `--all`(会删掉整个系列)
|
||||||
- 连续发多封邮件时,每封间隔 10 秒以上(Migadu SMTP 限频)
|
- 连续发多封邮件时,每封间隔 10 秒以上(Migadu SMTP 限频)
|
||||||
|
|
||||||
|
### Notesearch 笔记搜索
|
||||||
|
|
||||||
|
**目录**: `~/.openclaw/workspace/skills/notesearch/`
|
||||||
|
**配置**: `~/.openclaw/workspace/skills/notesearch/config.json`
|
||||||
|
**笔记库**: `/home/lyx/Documents/obsidian-yanxin`(Obsidian vault,git 管理)
|
||||||
|
|
||||||
|
基于向量搜索的笔记检索工具,使用 LlamaIndex + Ollama 嵌入模型索引 Obsidian 笔记。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NOTESEARCH=~/.openclaw/workspace/skills/notesearch/notesearch.sh
|
||||||
|
|
||||||
|
# 搜索笔记(返回相关片段 + 文件路径 + 相关性分数)
|
||||||
|
$NOTESEARCH search "allergy shots"
|
||||||
|
$NOTESEARCH search "project planning" --top-k 3
|
||||||
|
|
||||||
|
# 重建索引(笔记更新后需要重新索引)
|
||||||
|
$NOTESEARCH index
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
1. 用户提问 → 用 `search` 找到相关笔记片段
|
||||||
|
2. 如果需要完整内容 → `cat /home/lyx/Documents/obsidian-yanxin/<文件路径>`
|
||||||
|
3. 根据笔记内容回答用户问题
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 搜索基于语义(向量相似度),不仅仅是关键词匹配
|
||||||
|
- 笔记更新后需要运行 `$NOTESEARCH index` 重建索引
|
||||||
|
- 嵌入模型: `qwen3-embedding:0.6b`(通过 Ollama)
|
||||||
|
|
||||||
### OpenClaw Cron 定时任务
|
### OpenClaw Cron 定时任务
|
||||||
|
|
||||||
**规则**: 确定性 shell 任务用 `systemEvent`,需要 LLM 判断的用 `agentTurn`
|
**规则**: 确定性 shell 任务用 `systemEvent`,需要 LLM 判断的用 `agentTurn`
|
||||||
|
|||||||
@@ -1,22 +1,48 @@
|
|||||||
BEGIN:VCALENDAR
|
BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//OpenClaw//Calendar//EN
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:-//Apple Inc.//macOS 26.3.1//EN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:America/Los_Angeles
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
TZNAME:PDT
|
||||||
|
TZOFFSETFROM:-0800
|
||||||
|
TZOFFSETTO:-0700
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
|
TZNAME:PST
|
||||||
|
TZOFFSETFROM:-0700
|
||||||
|
TZOFFSETTO:-0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
BEGIN:VEVENT
|
BEGIN:VEVENT
|
||||||
SUMMARY:Allergy Shot (Sat)
|
ATTENDEE;CN=youlu@luyanxin.com;CUTYPE=INDIVIDUAL;EMAIL=youlu@luyanxin.com;P
|
||||||
DTSTART;TZID=America/Los_Angeles:20260328T090000
|
ARTSTAT=ACCEPTED:mailto:youlu@luyanxin.com
|
||||||
|
ATTENDEE;CUTYPE=UNKNOWN;EMAIL=Erica.Jiang@anderson.ucla.edu;ROLE=REQ-PARTIC
|
||||||
|
IPANT;RSVP=TRUE;SCHEDULE-STATUS=1.1:mailto:Erica.Jiang@anderson.ucla.edu
|
||||||
DTEND;TZID=America/Los_Angeles:20260328T093000
|
DTEND;TZID=America/Los_Angeles:20260328T093000
|
||||||
DTSTAMP:20260325T160918Z
|
DTSTAMP:20260403T160300Z
|
||||||
UID:1374d6ce-5f83-4c2e-b9a1-120cd2b949e5@openclaw
|
DTSTART;TZID=America/Los_Angeles:20260328T090000
|
||||||
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=SA
|
|
||||||
EXDATE;TZID=America/Los_Angeles:20260328T090000
|
EXDATE;TZID=America/Los_Angeles:20260328T090000
|
||||||
EXDATE;TZID=America/Los_Angeles:20260328T090000
|
LAST-MODIFIED:20260403T160258Z
|
||||||
ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE;SCHEDULE-STATUS=1.1:mailto:Erica.Ji
|
|
||||||
ang@anderson.ucla.edu
|
|
||||||
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
||||||
ORGANIZER;CN=Youlu:mailto:youlu@luyanxin.com
|
ORGANIZER;CN=Youlu;EMAIL=youlu@luyanxin.com:mailto:youlu@luyanxin.com
|
||||||
|
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=SA
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY:Allergy Shot (Sat)
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:1374d6ce-5f83-4c2e-b9a1-120cd2b949e5@openclaw
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260403T160258Z
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
TRIGGER:-P1D
|
TRIGGER:-P1D
|
||||||
|
UID:FADBDE52-87C0-40C8-96ED-B0DEC5A6D441
|
||||||
|
X-WR-ALARMUID:FADBDE52-87C0-40C8-96ED-B0DEC5A6D441
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
BEGIN:VCALENDAR
|
BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//OpenClaw//Calendar//EN
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:-//Apple Inc.//macOS 26.3.1//EN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:America/Los_Angeles
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
TZNAME:PDT
|
||||||
|
TZOFFSETFROM:-0800
|
||||||
|
TZOFFSETTO:-0700
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
|
TZNAME:PST
|
||||||
|
TZOFFSETFROM:-0700
|
||||||
|
TZOFFSETTO:-0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
BEGIN:VEVENT
|
BEGIN:VEVENT
|
||||||
SUMMARY:Allergy Shot (Tue)
|
ATTENDEE;CN=youlu@luyanxin.com;CUTYPE=INDIVIDUAL;EMAIL=youlu@luyanxin.com;P
|
||||||
DTSTART;TZID=America/Los_Angeles:20260331T143000
|
ARTSTAT=ACCEPTED:mailto:youlu@luyanxin.com
|
||||||
|
ATTENDEE;CUTYPE=UNKNOWN;EMAIL=Erica.Jiang@anderson.ucla.edu;ROLE=REQ-PARTIC
|
||||||
|
IPANT;RSVP=TRUE:mailto:Erica.Jiang@anderson.ucla.edu
|
||||||
DTEND;TZID=America/Los_Angeles:20260331T150000
|
DTEND;TZID=America/Los_Angeles:20260331T150000
|
||||||
DTSTAMP:20260325T160802Z
|
DTSTAMP:20260406T213025Z
|
||||||
UID:59c533e2-4153-42dd-b717-c42e104521d9@openclaw
|
DTSTART;TZID=America/Los_Angeles:20260331T143000
|
||||||
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=TU
|
|
||||||
EXDATE;TZID=America/Los_Angeles:20260331T143000
|
EXDATE;TZID=America/Los_Angeles:20260331T143000
|
||||||
ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE;SCHEDULE-STATUS=1.1:mailto:Erica.Ji
|
LAST-MODIFIED:20260406T213023Z
|
||||||
ang@anderson.ucla.edu
|
|
||||||
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
||||||
ORGANIZER;CN=Youlu:mailto:youlu@luyanxin.com
|
ORGANIZER;CN=Youlu;EMAIL=youlu@luyanxin.com:mailto:youlu@luyanxin.com
|
||||||
|
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=TU
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY:Allergy Shot (Tue)
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:59c533e2-4153-42dd-b717-c42e104521d9@openclaw
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260406T213023Z
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
TRIGGER:-P1D
|
TRIGGER:-P1D
|
||||||
|
UID:2850F4A5-B704-4A07-BC97-D284593D0CFB
|
||||||
|
X-WR-ALARMUID:2850F4A5-B704-4A07-BC97-D284593D0CFB
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
|
|||||||
@@ -1,20 +1,48 @@
|
|||||||
BEGIN:VCALENDAR
|
BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//OpenClaw//Calendar//EN
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:-//Apple Inc.//macOS 26.3.1//EN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:America/Los_Angeles
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
TZNAME:PDT
|
||||||
|
TZOFFSETFROM:-0800
|
||||||
|
TZOFFSETTO:-0700
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
|
TZNAME:PST
|
||||||
|
TZOFFSETFROM:-0700
|
||||||
|
TZOFFSETTO:-0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
BEGIN:VEVENT
|
BEGIN:VEVENT
|
||||||
SUMMARY:Allergy Shot (Thu)
|
ATTENDEE;CN=youlu@luyanxin.com;CUTYPE=INDIVIDUAL;EMAIL=youlu@luyanxin.com;P
|
||||||
DTSTART;TZID=America/Los_Angeles:20260326T073000
|
ARTSTAT=ACCEPTED:mailto:youlu@luyanxin.com
|
||||||
|
ATTENDEE;CUTYPE=UNKNOWN;EMAIL=Erica.Jiang@anderson.ucla.edu;ROLE=REQ-PARTIC
|
||||||
|
IPANT;RSVP=TRUE:mailto:Erica.Jiang@anderson.ucla.edu
|
||||||
DTEND;TZID=America/Los_Angeles:20260326T080000
|
DTEND;TZID=America/Los_Angeles:20260326T080000
|
||||||
DTSTAMP:20260325T160851Z
|
DTSTAMP:20260401T194350Z
|
||||||
UID:7b822ffc-1d3b-4a95-8835-f2e75a0f583d@openclaw
|
DTSTART;TZID=America/Los_Angeles:20260326T073000
|
||||||
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=TH
|
LAST-MODIFIED:20260401T143011Z
|
||||||
ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE;SCHEDULE-STATUS=1.1:mailto:Erica.Ji
|
|
||||||
ang@anderson.ucla.edu
|
|
||||||
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
LOCATION:11965 Venice Blvd. #300\, Los Angeles\, CA 90066
|
||||||
ORGANIZER;CN=Youlu:mailto:youlu@luyanxin.com
|
ORGANIZER;CN=youlu@luyanxin.com;EMAIL=youlu@luyanxin.com:mailto:youlu@luyan
|
||||||
|
xin.com
|
||||||
|
RRULE:FREQ=WEEKLY;COUNT=13;BYDAY=TH
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY:Allergy Shot (Thu)
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:7b822ffc-1d3b-4a95-8835-f2e75a0f583d@openclaw
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260401T143011Z
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
TRIGGER:-P1D
|
TRIGGER:-P1D
|
||||||
|
UID:42D85383-621D-438A-AC74-3794A2B54943
|
||||||
|
X-WR-ALARMUID:42D85383-621D-438A-AC74-3794A2B54943
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:-//Apple Inc.//macOS 26.3.1//EN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:America/Los_Angeles
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
TZNAME:PDT
|
||||||
|
TZOFFSETFROM:-0800
|
||||||
|
TZOFFSETTO:-0700
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
|
TZNAME:PST
|
||||||
|
TZOFFSETFROM:-0700
|
||||||
|
TZOFFSETTO:-0800
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;CN=youlu@luyanxin.com;CUTYPE=INDIVIDUAL;EMAIL=youlu@luyanxin.com;P
|
||||||
|
ARTSTAT=ACCEPTED:mailto:youlu@luyanxin.com
|
||||||
|
ATTENDEE;CUTYPE=UNKNOWN;EMAIL=Erica.Jiang@anderson.ucla.edu;ROLE=REQ-PARTIC
|
||||||
|
IPANT;RSVP=TRUE:mailto:Erica.Jiang@anderson.ucla.edu
|
||||||
|
DESCRIPTION:带二狗去 Shane Veterinary Medical Center 看病
|
||||||
|
DTEND;TZID=America/Los_Angeles:20260406T163000
|
||||||
|
DTSTAMP:20260405T223800Z
|
||||||
|
DTSTART;TZID=America/Los_Angeles:20260406T153000
|
||||||
|
LAST-MODIFIED:20260405T223757Z
|
||||||
|
LOCATION:Shane Veterinary Medical Center
|
||||||
|
ORGANIZER;CN=youlu@luyanxin.com;EMAIL=youlu@luyanxin.com:mailto:youlu@luyan
|
||||||
|
xin.com
|
||||||
|
SEQUENCE:0
|
||||||
|
STATUS:CONFIRMED
|
||||||
|
SUMMARY:带二狗看病
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:b1c9bb0f-89ed-4ada-a88c-74b3d549274a@openclaw
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260405T223757Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder: 带二狗看病
|
||||||
|
TRIGGER:-P1D
|
||||||
|
UID:AB8511BE-ED23-4BCC-93C4-E79A68AA4DBD
|
||||||
|
X-WR-ALARMUID:AB8511BE-ED23-4BCC-93C4-E79A68AA4DBD
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Apple Inc.//iOS 26.3.1//EN
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20260405T035527Z
|
||||||
|
CREATED:20260403T162742Z
|
||||||
|
DTSTAMP:20260404T135144Z
|
||||||
|
DUE;VALUE=DATE:20260405
|
||||||
|
LAST-MODIFIED:20260405T035527Z
|
||||||
|
PERCENT-COMPLETE:100
|
||||||
|
PRIORITY:1
|
||||||
|
SEQUENCE:2
|
||||||
|
STATUS:COMPLETED
|
||||||
|
SUMMARY:报税
|
||||||
|
UID:2977e496-0ce9-42c5-ae91-eabfd3837b82@openclaw
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260404T135140Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
|
TRIGGER:-P1D
|
||||||
|
UID:A56270D2-4179-4B9C-8D6D-9A316ECDA136
|
||||||
|
X-WR-ALARMUID:A56270D2-4179-4B9C-8D6D-9A316ECDA136
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Apple Inc.//iOS 26.3.1//EN
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20260404T225027Z
|
||||||
|
CREATED:20260403T162816Z
|
||||||
|
DTSTAMP:20260404T135144Z
|
||||||
|
DUE;VALUE=DATE:20260405
|
||||||
|
LAST-MODIFIED:20260404T225027Z
|
||||||
|
PERCENT-COMPLETE:100
|
||||||
|
PRIORITY:1
|
||||||
|
SEQUENCE:2
|
||||||
|
STATUS:COMPLETED
|
||||||
|
SUMMARY:报销IUI费用到FSA
|
||||||
|
UID:906202b8-6df5-4ac2-bf3b-e59ffaddccd6@openclaw
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACKNOWLEDGED:20260404T135140Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
|
TRIGGER:-P1D
|
||||||
|
UID:0A2D0B7D-0FDD-48B4-9E27-C54EBD3B120B
|
||||||
|
X-WR-ALARMUID:0A2D0B7D-0FDD-48B4-9E27-C54EBD3B120B
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
@@ -3,17 +3,19 @@ VERSION:2.0
|
|||||||
PRODID:-//Apple Inc.//iOS 26.3.1//EN
|
PRODID:-//Apple Inc.//iOS 26.3.1//EN
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
BEGIN:VTODO
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20260405T154326Z
|
||||||
CREATED:20260327T164116Z
|
CREATED:20260327T164116Z
|
||||||
DTSTAMP:20260330T161515Z
|
DTSTAMP:20260403T160300Z
|
||||||
DUE;VALUE=DATE:20260403
|
DUE;VALUE=DATE:20260403
|
||||||
LAST-MODIFIED:20260330T215759Z
|
LAST-MODIFIED:20260405T154326Z
|
||||||
|
PERCENT-COMPLETE:100
|
||||||
PRIORITY:5
|
PRIORITY:5
|
||||||
SEQUENCE:3
|
SEQUENCE:4
|
||||||
STATUS:NEEDS-ACTION
|
STATUS:COMPLETED
|
||||||
SUMMARY:跟进iui保险报销
|
SUMMARY:跟进iui保险报销
|
||||||
UID:aa4868bb-b602-418f-8067-20d00fe2b27c@openclaw
|
UID:aa4868bb-b602-418f-8067-20d00fe2b27c@openclaw
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
ACKNOWLEDGED:20260330T161514Z
|
ACKNOWLEDGED:20260403T160300Z
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
DESCRIPTION:Reminder
|
DESCRIPTION:Reminder
|
||||||
TRIGGER:-P1D
|
TRIGGER:-P1D
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ BEGIN:VCALENDAR
|
|||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//OpenClaw//Calendar//EN
|
PRODID:-//OpenClaw//Calendar//EN
|
||||||
BEGIN:VTODO
|
BEGIN:VTODO
|
||||||
CREATED:20260322T214938Z
|
CREATED:20260403T162701Z
|
||||||
DESCRIPTION:询问iui报销相关事宜
|
DESCRIPTION:询问iui报销相关事宜
|
||||||
DTSTAMP:20260322T214938Z
|
DTSTAMP:20260403T162701Z
|
||||||
DUE;VALUE=DATE:20260404
|
DUE;VALUE=DATE:20260410
|
||||||
PRIORITY:5
|
PRIORITY:5
|
||||||
STATUS:NEEDS-ACTION
|
STATUS:NEEDS-ACTION
|
||||||
SUMMARY:打电话给progyny问iui报销
|
SUMMARY:打电话给progyny问iui报销
|
||||||
UID:1a6aec16-5981-4035-a8a1-2ca1f0854956@openclaw
|
UID:bbfa2934-f7fd-4444-9c33-e8569f9a7ceb@openclaw
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
DESCRIPTION:Todo: 打电话给progyny问iui报销
|
DESCRIPTION:Todo: 打电话给progyny问iui报销
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//OpenClaw//Calendar//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
CREATED:20260403T163408Z
|
||||||
|
DTSTAMP:20260403T163408Z
|
||||||
|
DUE;VALUE=DATE:20260408
|
||||||
|
PRIORITY:5
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:发complain信
|
||||||
|
UID:d708aad8-9f8c-4e39-806b-f7dfc29e1d88@openclaw
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Todo: 发complain信
|
||||||
|
TRIGGER:-P1D
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
76
skills/notesearch/README.md
Normal file
76
skills/notesearch/README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# notesearch
|
||||||
|
|
||||||
|
Local vector search over markdown notes using LlamaIndex + Ollama.
|
||||||
|
|
||||||
|
Point it at an Obsidian vault (or any folder of `.md` files), build a vector index, and search by meaning — not just keywords.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/skills/notesearch
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Ollama running locally with an embedding model pulled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama pull qwen3-embedding:0.6b
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Build the index
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./notesearch.sh index --vault /path/to/vault
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./notesearch.sh search "where do I get my allergy shots"
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
[0.87] Health/allergy.md
|
||||||
|
Started allergy shots in March 2026. Clinic is at 123 Main St.
|
||||||
|
|
||||||
|
[0.72] Daily/2026-03-25.md
|
||||||
|
Went to allergy appointment today.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Edit `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vault": "/home/lyx/Documents/obsidian-yanxin",
|
||||||
|
"index_dir": null,
|
||||||
|
"ollama_url": "http://localhost:11434",
|
||||||
|
"embedding_model": "qwen3-embedding:0.6b"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Values can also be set via flags or env vars. Priority: **flag > env var > config.json > fallback**.
|
||||||
|
|
||||||
|
| Flag | Env var | Config key | Default |
|
||||||
|
|------|---------|------------|---------|
|
||||||
|
| `--vault` | `NOTESEARCH_VAULT` | `vault` | `/home/lyx/Documents/obsidian-yanxin` |
|
||||||
|
| `--index-dir` | `NOTESEARCH_INDEX_DIR` | `index_dir` | `<vault>/.index/` |
|
||||||
|
| `--ollama-url` | `NOTESEARCH_OLLAMA_URL` | `ollama_url` | `http://localhost:11434` |
|
||||||
|
| `--embedding-model` | `NOTESEARCH_EMBEDDING_MODEL` | `embedding_model` | `qwen3-embedding:0.6b` |
|
||||||
|
| `--top-k` | — | — | `5` |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Index**: reads all `.md` files, splits on markdown headings, embeds each chunk via Ollama, stores vectors locally
|
||||||
|
2. **Search**: embeds your query, finds the most similar chunks, returns them with file paths and relevance scores
|
||||||
4
skills/notesearch/_meta.json
Normal file
4
skills/notesearch/_meta.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"slug": "notesearch",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
6
skills/notesearch/config.json
Normal file
6
skills/notesearch/config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"vault": "/home/lyx/Documents/obsidian-yanxin",
|
||||||
|
"index_dir": null,
|
||||||
|
"ollama_url": "http://localhost:11434",
|
||||||
|
"embedding_model": "qwen3-embedding:0.6b"
|
||||||
|
}
|
||||||
7
skills/notesearch/notesearch.sh
Executable file
7
skills/notesearch/notesearch.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
exec uv run python -m notesearch "$@"
|
||||||
0
skills/notesearch/notesearch/__init__.py
Normal file
0
skills/notesearch/notesearch/__init__.py
Normal file
5
skills/notesearch/notesearch/__main__.py
Normal file
5
skills/notesearch/notesearch/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Allow running as `python -m notesearch`."""
|
||||||
|
|
||||||
|
from notesearch.cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
89
skills/notesearch/notesearch/cli.py
Normal file
89
skills/notesearch/notesearch/cli.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""CLI entry point for notesearch."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from notesearch.core import (
|
||||||
|
FALLBACK_EMBEDDING_MODEL,
|
||||||
|
FALLBACK_OLLAMA_URL,
|
||||||
|
FALLBACK_VAULT,
|
||||||
|
build_index,
|
||||||
|
get_config_value,
|
||||||
|
search,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve(flag_value: str | None, env_name: str, config_key: str, fallback: str) -> str:
|
||||||
|
"""Resolve a value with priority: flag > env var > config.json > fallback."""
|
||||||
|
if flag_value:
|
||||||
|
return flag_value
|
||||||
|
env = os.environ.get(env_name)
|
||||||
|
if env:
|
||||||
|
return env
|
||||||
|
return get_config_value(config_key, fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_index(args: argparse.Namespace) -> None:
|
||||||
|
vault = _resolve(args.vault, "NOTESEARCH_VAULT", "vault", FALLBACK_VAULT)
|
||||||
|
index_dir = _resolve(args.index_dir, "NOTESEARCH_INDEX_DIR", "index_dir", "") or None
|
||||||
|
ollama_url = _resolve(args.ollama_url, "NOTESEARCH_OLLAMA_URL", "ollama_url", FALLBACK_OLLAMA_URL)
|
||||||
|
model = _resolve(args.model, "NOTESEARCH_EMBEDDING_MODEL", "embedding_model", FALLBACK_EMBEDDING_MODEL)
|
||||||
|
|
||||||
|
print(f"Indexing vault: {vault}")
|
||||||
|
print(f"Model: {model}")
|
||||||
|
idx_path = build_index(vault, index_dir, ollama_url, model)
|
||||||
|
print(f"Index saved to: {idx_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_search(args: argparse.Namespace) -> None:
|
||||||
|
vault = _resolve(args.vault, "NOTESEARCH_VAULT", "vault", FALLBACK_VAULT)
|
||||||
|
index_dir = _resolve(args.index_dir, "NOTESEARCH_INDEX_DIR", "index_dir", "") or None
|
||||||
|
ollama_url = _resolve(args.ollama_url, "NOTESEARCH_OLLAMA_URL", "ollama_url", FALLBACK_OLLAMA_URL)
|
||||||
|
|
||||||
|
results = search(args.query, vault, index_dir, ollama_url, args.top_k)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print("No results found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
print(f"[{r['score']:.2f}] {r['file']}")
|
||||||
|
print(r["text"])
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="notesearch",
|
||||||
|
description="Local vector search over markdown notes",
|
||||||
|
)
|
||||||
|
parser.add_argument("--vault", help="Path to the Obsidian vault")
|
||||||
|
parser.add_argument("--index-dir", help="Path to store/load the index")
|
||||||
|
parser.add_argument("--ollama-url", help="Ollama API URL")
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# index
|
||||||
|
idx_parser = subparsers.add_parser("index", help="Build the search index")
|
||||||
|
idx_parser.add_argument("--embedding-model", dest="model", help="Ollama embedding model name")
|
||||||
|
|
||||||
|
# search
|
||||||
|
search_parser = subparsers.add_parser("search", help="Search the notes")
|
||||||
|
search_parser.add_argument("query", help="Search query")
|
||||||
|
search_parser.add_argument("--top-k", type=int, default=5, help="Number of results")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.command == "index":
|
||||||
|
cmd_index(args)
|
||||||
|
elif args.command == "search":
|
||||||
|
cmd_search(args)
|
||||||
|
except (FileNotFoundError, ValueError) as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
124
skills/notesearch/notesearch/core.py
Normal file
124
skills/notesearch/notesearch/core.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""Core indexing and search logic."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from llama_index.core import (
|
||||||
|
SimpleDirectoryReader,
|
||||||
|
StorageContext,
|
||||||
|
VectorStoreIndex,
|
||||||
|
load_index_from_storage,
|
||||||
|
)
|
||||||
|
from llama_index.core.node_parser import MarkdownNodeParser
|
||||||
|
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||||
|
|
||||||
|
|
||||||
|
FALLBACK_VAULT = "/home/lyx/Documents/obsidian-yanxin"
|
||||||
|
FALLBACK_EMBEDDING_MODEL = "qwen3-embedding:0.6b"
|
||||||
|
FALLBACK_OLLAMA_URL = "http://localhost:11434"
|
||||||
|
METADATA_FILE = "notesearch_meta.json"
|
||||||
|
CONFIG_FILE = Path(__file__).parent.parent / "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> dict:
|
||||||
|
"""Load config from config.json. Returns empty dict if not found."""
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
return json.loads(CONFIG_FILE.read_text())
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_value(key: str, fallback: str) -> str:
|
||||||
|
"""Get a config value from config.json, with a hardcoded fallback."""
|
||||||
|
config = load_config()
|
||||||
|
return config.get(key) or fallback
|
||||||
|
|
||||||
|
|
||||||
|
def _get_index_dir(vault_path: str, index_dir: str | None) -> Path:
|
||||||
|
if index_dir:
|
||||||
|
return Path(index_dir)
|
||||||
|
return Path(vault_path) / ".index"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_embed_model(ollama_url: str, model: str) -> OllamaEmbedding:
|
||||||
|
return OllamaEmbedding(model_name=model, base_url=ollama_url)
|
||||||
|
|
||||||
|
|
||||||
|
def build_index(
|
||||||
|
vault_path: str = FALLBACK_VAULT,
|
||||||
|
index_dir: str | None = None,
|
||||||
|
ollama_url: str = FALLBACK_OLLAMA_URL,
|
||||||
|
model: str = FALLBACK_EMBEDDING_MODEL,
|
||||||
|
) -> Path:
|
||||||
|
"""Build a vector index from markdown files in the vault."""
|
||||||
|
vault = Path(vault_path)
|
||||||
|
if not vault.is_dir():
|
||||||
|
raise FileNotFoundError(f"Vault not found: {vault_path}")
|
||||||
|
|
||||||
|
idx_path = _get_index_dir(vault_path, index_dir)
|
||||||
|
idx_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Check for markdown files before loading (SimpleDirectoryReader raises
|
||||||
|
# its own error on empty dirs, but we want a clearer message)
|
||||||
|
md_files = list(vault.rglob("*.md"))
|
||||||
|
if not md_files:
|
||||||
|
raise ValueError(f"No markdown files found in {vault_path}")
|
||||||
|
|
||||||
|
documents = SimpleDirectoryReader(
|
||||||
|
str(vault),
|
||||||
|
recursive=True,
|
||||||
|
required_exts=[".md"],
|
||||||
|
).load_data()
|
||||||
|
|
||||||
|
embed_model = _get_embed_model(ollama_url, model)
|
||||||
|
parser = MarkdownNodeParser()
|
||||||
|
nodes = parser.get_nodes_from_documents(documents)
|
||||||
|
|
||||||
|
index = VectorStoreIndex(nodes, embed_model=embed_model)
|
||||||
|
index.storage_context.persist(persist_dir=str(idx_path))
|
||||||
|
|
||||||
|
# Save metadata so we can detect model mismatches
|
||||||
|
meta = {"model": model, "ollama_url": ollama_url, "vault_path": vault_path}
|
||||||
|
(idx_path / METADATA_FILE).write_text(json.dumps(meta, indent=2))
|
||||||
|
|
||||||
|
return idx_path
|
||||||
|
|
||||||
|
|
||||||
|
def search(
|
||||||
|
query: str,
|
||||||
|
vault_path: str = FALLBACK_VAULT,
|
||||||
|
index_dir: str | None = None,
|
||||||
|
ollama_url: str = FALLBACK_OLLAMA_URL,
|
||||||
|
top_k: int = 5,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Search the index and return matching chunks."""
|
||||||
|
idx_path = _get_index_dir(vault_path, index_dir)
|
||||||
|
|
||||||
|
if not idx_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Index not found at {idx_path}. Run 'notesearch index' first."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load metadata and check model
|
||||||
|
meta_file = idx_path / METADATA_FILE
|
||||||
|
if meta_file.exists():
|
||||||
|
meta = json.loads(meta_file.read_text())
|
||||||
|
model = meta.get("model", FALLBACK_EMBEDDING_MODEL)
|
||||||
|
else:
|
||||||
|
model = FALLBACK_EMBEDDING_MODEL
|
||||||
|
|
||||||
|
embed_model = _get_embed_model(ollama_url, model)
|
||||||
|
|
||||||
|
storage_context = StorageContext.from_defaults(persist_dir=str(idx_path))
|
||||||
|
index = load_index_from_storage(storage_context, embed_model=embed_model)
|
||||||
|
|
||||||
|
retriever = index.as_retriever(similarity_top_k=top_k)
|
||||||
|
results = retriever.retrieve(query)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"score": round(r.score, 4),
|
||||||
|
"file": r.node.metadata.get("file_path", "unknown"),
|
||||||
|
"text": r.node.text,
|
||||||
|
}
|
||||||
|
for r in results
|
||||||
|
]
|
||||||
18
skills/notesearch/pyproject.toml
Normal file
18
skills/notesearch/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "notesearch"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Local vector search over markdown notes using LlamaIndex + Ollama"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"llama-index",
|
||||||
|
"llama-index-embeddings-ollama",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
notesearch = "notesearch.cli:main"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest"]
|
||||||
0
skills/notesearch/tests/__init__.py
Normal file
0
skills/notesearch/tests/__init__.py
Normal file
152
skills/notesearch/tests/test_core.py
Normal file
152
skills/notesearch/tests/test_core.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"""Tests for notesearch core functionality."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from llama_index.core.base.embeddings.base import BaseEmbedding
|
||||||
|
from notesearch.core import FALLBACK_EMBEDDING_MODEL, METADATA_FILE, build_index, search
|
||||||
|
|
||||||
|
|
||||||
|
class FakeEmbedding(BaseEmbedding):
|
||||||
|
"""Deterministic embedding model for testing."""
|
||||||
|
|
||||||
|
model_name: str = "test-model"
|
||||||
|
|
||||||
|
def _get_text_embedding(self, text: str) -> list[float]:
|
||||||
|
h = hashlib.md5(text.encode()).digest()
|
||||||
|
return [b / 255.0 for b in h] * 48 # 768-dim
|
||||||
|
|
||||||
|
def _get_query_embedding(self, query: str) -> list[float]:
|
||||||
|
return self._get_text_embedding(query)
|
||||||
|
|
||||||
|
async def _aget_text_embedding(self, text: str) -> list[float]:
|
||||||
|
return self._get_text_embedding(text)
|
||||||
|
|
||||||
|
async def _aget_query_embedding(self, query: str) -> list[float]:
|
||||||
|
return self._get_text_embedding(query)
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_embed_model(*args: Any, **kwargs: Any) -> FakeEmbedding:
|
||||||
|
return FakeEmbedding()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_vault(tmp_path: Path) -> Path:
|
||||||
|
"""Create a temporary vault with sample markdown files."""
|
||||||
|
vault = tmp_path / "vault"
|
||||||
|
vault.mkdir()
|
||||||
|
|
||||||
|
(vault / "health").mkdir()
|
||||||
|
(vault / "health" / "allergy.md").write_text(
|
||||||
|
"# Allergy Treatment\n\n"
|
||||||
|
"Started allergy shots in March 2026.\n"
|
||||||
|
"Weekly schedule: Tuesday and Thursday.\n"
|
||||||
|
"Clinic is at 123 Main St.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
(vault / "work").mkdir()
|
||||||
|
(vault / "work" / "project-alpha.md").write_text(
|
||||||
|
"# Project Alpha\n\n"
|
||||||
|
"## Goals\n"
|
||||||
|
"Launch the new API by Q2.\n"
|
||||||
|
"Migrate all users to v2 endpoints.\n\n"
|
||||||
|
"## Status\n"
|
||||||
|
"Backend is 80% done. Frontend blocked on design review.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
(vault / "recipes.md").write_text(
|
||||||
|
"# Favorite Recipes\n\n"
|
||||||
|
"## Pasta Carbonara\n"
|
||||||
|
"Eggs, pecorino, guanciale, black pepper.\n"
|
||||||
|
"Cook pasta al dente, mix off heat.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return vault
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def empty_vault(tmp_path: Path) -> Path:
|
||||||
|
"""Create an empty vault directory."""
|
||||||
|
vault = tmp_path / "empty_vault"
|
||||||
|
vault.mkdir()
|
||||||
|
return vault
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildIndex:
|
||||||
|
def test_missing_vault(self, tmp_path: Path) -> None:
|
||||||
|
with pytest.raises(FileNotFoundError, match="Vault not found"):
|
||||||
|
build_index(vault_path=str(tmp_path / "nonexistent"))
|
||||||
|
|
||||||
|
def test_empty_vault(self, empty_vault: Path) -> None:
|
||||||
|
with pytest.raises(ValueError, match="No markdown files found"):
|
||||||
|
build_index(vault_path=str(empty_vault))
|
||||||
|
|
||||||
|
@patch("notesearch.core._get_embed_model", _mock_embed_model)
|
||||||
|
def test_builds_index(self, sample_vault: Path, tmp_path: Path) -> None:
|
||||||
|
index_dir = tmp_path / "index"
|
||||||
|
idx_path = build_index(
|
||||||
|
vault_path=str(sample_vault),
|
||||||
|
index_dir=str(index_dir),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert idx_path == index_dir
|
||||||
|
assert idx_path.exists()
|
||||||
|
assert (idx_path / METADATA_FILE).exists()
|
||||||
|
|
||||||
|
meta = json.loads((idx_path / METADATA_FILE).read_text())
|
||||||
|
assert meta["vault_path"] == str(sample_vault)
|
||||||
|
assert "model" in meta
|
||||||
|
|
||||||
|
@patch("notesearch.core._get_embed_model", _mock_embed_model)
|
||||||
|
def test_index_stores_model_metadata(self, sample_vault: Path, tmp_path: Path) -> None:
|
||||||
|
index_dir = tmp_path / "index"
|
||||||
|
build_index(
|
||||||
|
vault_path=str(sample_vault),
|
||||||
|
index_dir=str(index_dir),
|
||||||
|
model="custom-model",
|
||||||
|
)
|
||||||
|
|
||||||
|
meta = json.loads((index_dir / METADATA_FILE).read_text())
|
||||||
|
assert meta["model"] == "custom-model"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSearch:
|
||||||
|
def test_missing_index(self, tmp_path: Path) -> None:
|
||||||
|
with pytest.raises(FileNotFoundError, match="Index not found"):
|
||||||
|
search("test query", vault_path=str(tmp_path))
|
||||||
|
|
||||||
|
@patch("notesearch.core._get_embed_model", _mock_embed_model)
|
||||||
|
def test_search_returns_results(self, sample_vault: Path, tmp_path: Path) -> None:
|
||||||
|
index_dir = tmp_path / "index"
|
||||||
|
build_index(vault_path=str(sample_vault), index_dir=str(index_dir))
|
||||||
|
|
||||||
|
results = search(
|
||||||
|
"allergy shots",
|
||||||
|
vault_path=str(sample_vault),
|
||||||
|
index_dir=str(index_dir),
|
||||||
|
top_k=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) > 0
|
||||||
|
assert all("score" in r for r in results)
|
||||||
|
assert all("file" in r for r in results)
|
||||||
|
assert all("text" in r for r in results)
|
||||||
|
|
||||||
|
@patch("notesearch.core._get_embed_model", _mock_embed_model)
|
||||||
|
def test_search_respects_top_k(self, sample_vault: Path, tmp_path: Path) -> None:
|
||||||
|
index_dir = tmp_path / "index"
|
||||||
|
build_index(vault_path=str(sample_vault), index_dir=str(index_dir))
|
||||||
|
|
||||||
|
results = search(
|
||||||
|
"anything",
|
||||||
|
vault_path=str(sample_vault),
|
||||||
|
index_dir=str(index_dir),
|
||||||
|
top_k=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(results) == 1
|
||||||
2154
skills/notesearch/uv.lock
generated
Normal file
2154
skills/notesearch/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user