Add app_settings migration, settings UI, and OpenAI-compatible httpx LLM client with mocked tests. Preserve API keys on blank form submissions, require a fresh key when base_url changes, and keep AI search settings untouched for step 3. Update docs/design LLM integration and step 3 AI search notes, including prompt contract and extra-hints planning.
This commit is contained in:
@@ -199,6 +199,7 @@ More settings are coming; adding columns to an *existing* table costs a migratio
|
||||
| `llm_model` | 模型名 / model name | (空 / empty) |
|
||||
| `llm_api_key` | API Key(明文 / plaintext,见 §7) | (空 / empty) |
|
||||
| `ai_search_enabled` | AI 搜索功能开关 / AI-search feature toggle | `false` |
|
||||
| `ai_search_extra_hints` | AI 搜索:可选「额外领域提示」,追加到默认系统提示词(step 3 引入)/ optional extra domain hints appended to the default prompt | (空 / empty) |
|
||||
|
||||
> 读写封装 / Access helpers:`get_app_settings(db) -> LLMConfig`(dataclass 视图)与 `save_app_settings(db, ...)`,供路由与 `app/llm.py` 复用。
|
||||
> Helpers `get_app_settings(db) -> LLMConfig` and `save_app_settings(db, ...)`, reused by routes and `app/llm.py`.
|
||||
@@ -209,7 +210,7 @@ OpenAI 兼容的薄客户端,基于 `httpx`,**无新依赖** / A thin OpenAI
|
||||
|
||||
- `is_configured(cfg) -> bool`:开关开启且 `model`/`api_key` 齐全。
|
||||
- `test_connection(cfg) -> Result`:发一个最小请求验证 `base_url`/`model`/`api_key`,供配置页"测试连接"用。
|
||||
- `expand_query(cfg, query) -> list[str]`:把查询词扩成一批近义/相关词(本轮 AI 搜索用,见 §5)。
|
||||
- `expand_query(cfg, query) -> list[str]`:把查询词扩成一批近义/相关词(提示词与输出契约见 §5.2)。
|
||||
- `analyze_image(...)`:**本轮不实现**,仅在文档中预留为未来接口(图片分析轮次)。Reserved for a future round, not implemented now.
|
||||
|
||||
要点 / Notes:
|
||||
@@ -251,7 +252,22 @@ When disabled/unconfigured: the settings page still works; the AI-search button
|
||||
- **只把查询词发出去 / Only the query leaves**,不外泄物品清单;token 恒定、不随上千件物品增长。
|
||||
Only the query is sent; the inventory is not. Token cost is constant and does not grow with thousands of items.
|
||||
|
||||
### 5.2 实现接口 / Implementation seam
|
||||
### 5.2 提示词与输出契约 / Prompt & Output Contract
|
||||
|
||||
`expand_query` 的**质量**取决于提示词,**集成稳定性**取决于输出契约——两者都在代码侧掌控(决策 C)。
|
||||
Quality hinges on the prompt; integration stability hinges on the output contract — both are code-controlled (decision C).
|
||||
|
||||
- **基础系统提示词写死在 `app/llm.py`(用户改不坏)/ Base system prompt hardcoded:** 框定搬家/家居场景,要求"列出用户可能用来命名同一物品的相关词(近义、别称、上位类别、具体品类)";语言跟随查询;最多约 8 个;不解释、不造无关词。
|
||||
Frames the moving/household domain, asks for related naming terms, follows the query's language, caps the count, no prose.
|
||||
- **可选「额外领域提示」/ Optional extra hints:** KV `ai_search_extra_hints`(设置页一个多行输入,默认空)。非空时**追加**到基础提示词之后,供业主微调倾向(如"厨房用品多,偏向厨具类")。**它只能补充,不能改写输出格式。**
|
||||
An optional free-text setting appended to the base prompt; it can only add guidance, never alter the output format.
|
||||
- **输出契约(代码强制,与提示词解耦)/ Output contract (code-enforced):** 要求模型只返回 **JSON 字符串数组**;解析时去掉 ` ```json ` 围栏 → `json.loads` → 失败按行/逗号兜底 → 再不行返回 `[]`。`expand_query` 只返回扩展词;**原词由 `ai_search` 并入并去重**,数量在代码侧再封顶一次。
|
||||
Require a JSON string array; tolerant parse with fallbacks to `[]`. `ai_search` adds the original term and dedupes; the count is capped in code.
|
||||
- **客户端参数 / Client params:** 低 temperature、较小 max_tokens、设超时。Low temperature, small max_tokens, a timeout.
|
||||
- **措辞留松 / Wording left loose:** 默认提示词的具体字句可在 step-3 实测中迭代,不在文档里冻死。
|
||||
Exact default wording can be iterated during step-3 testing.
|
||||
|
||||
### 5.3 实现接口 / Implementation seam
|
||||
|
||||
- 路由层扩展现有 `GET /search`:增加 `ai=1` 触发位(如 `GET /search?q=锅&ai=1`),保持单页、可收藏、SSR 友好。
|
||||
Extend the existing `GET /search` with an `ai=1` trigger (e.g. `/search?q=…&ai=1`), staying single-page and bookmarkable.
|
||||
@@ -263,7 +279,7 @@ When disabled/unconfigured: the settings page still works; the AI-search button
|
||||
- 本轮检索范围=`name` + `note`(`image_description` 本轮不存在)。
|
||||
Search scope this round = `name` + `note` (no `image_description` yet).
|
||||
|
||||
### 5.3 降级 / Degradation
|
||||
### 5.4 降级 / Degradation
|
||||
|
||||
AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示并回退到普通结果。
|
||||
AI off/unconfigured → no button (or a hint to `/settings`); on failure → a friendly message, fall back to normal results.
|
||||
@@ -334,3 +350,4 @@ AI off/unconfigured → no button (or a hint to `/settings`); on failure → a f
|
||||
| D8 | AI 搜索 v1=查询词扩展 / query-term expansion | 上千件物品下可扩展、不外泄清单、token 恒定。 |
|
||||
| D9 | 检索做成可替换 seam / pluggable retrieval | 未来换嵌入式语义搜索时上层不动。 |
|
||||
| D10 | 图片分析不在本轮 / image analysis deferred | 业主本轮三件事不含它;架构预留接口。 |
|
||||
| D11 | AI 搜索提示词:默认写死 + 可选「额外领域提示」;输出契约由代码强制 / hardcoded default prompt + optional extra-hints, code-enforced JSON contract | 保证解析稳定(用户改不坏),又给业主一点不改代码即可微调的空间。 |
|
||||
|
||||
@@ -17,7 +17,9 @@ A **persistent** "AI search" action on the search page that broadens results via
|
||||
|
||||
- 现有搜索:`app/main.py::_build_search_results(db, query)` 对 `Box`/`Item`/`SubItem` 的 `name` 与 `note` 做大小写不敏感 `LIKE`,返回结果列表;路由 `GET /search`(函数 `search_page`,参数 `q`)渲染 `app/templates/search/index.html`。
|
||||
Existing search: `_build_search_results(db, query)` does case-insensitive `LIKE` over name/note; route `GET /search` renders `search/index.html`.
|
||||
- 步骤 2 已提供:`app/llm.py::expand_query(cfg, query) -> list[str]`、配置读取 `get_app_settings(db)`、开关 `ai_search_enabled` 与 `is_configured(cfg)`。
|
||||
- 步骤 2 已提供:`app/llm.py::expand_query(cfg, query) -> list[str]`、配置读取 `get_app_settings(db)`、开关 `ai_search_enabled` 与 `is_configured(cfg)`、设置页 `app/templates/settings/form.html`。
|
||||
- 本步**新增**配置项 `ai_search_extra_hints`(可选「额外领域提示」)并在设置页加一个多行输入——这是本步**唯一**触及设置页之处。
|
||||
This step adds the `ai_search_extra_hints` setting + a textarea on the settings page (the only settings-page change here).
|
||||
- 本轮检索范围=`name` + `note`(`image_description` 本轮不存在,属未来图片分析轮次)。
|
||||
Search scope = `name` + `note` (no `image_description` this round).
|
||||
|
||||
@@ -29,12 +31,20 @@ A **persistent** "AI search" action on the search page that broadens results via
|
||||
Trigger → expand → OR-`LIKE` over the original + expanded terms → render with a banner of the expansion. Only the query leaves.
|
||||
- **可替换的检索 seam。** 把 AI 检索抽成一个函数(如 `ai_search(db, query) -> (expanded_terms, results)`),本轮内部=查询词扩展 + 本地 `LIKE`;**未来换成向量嵌入 + 相似度时,路由与模板不变**。
|
||||
Wrap AI retrieval behind a swappable seam so embeddings can replace it later without touching route/template.
|
||||
- **提示词(决策 C,详见设计 §5.2)。** 基础系统提示词**写死在 `app/llm.py`**;设置页可选的 `ai_search_extra_hints` 非空时**追加**到其后;**输出契约由代码强制**(要求 JSON 字符串数组 → 容忍性解析 → 失败返回 `[]`),用户改 hints 也改不坏解析。
|
||||
Base prompt hardcoded; optional extra hints appended; output contract (JSON array → tolerant parse → `[]`) enforced in code.
|
||||
- **优雅降级。** AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示 + 回退普通结果。
|
||||
|
||||
---
|
||||
|
||||
## 任务 / Tasks
|
||||
|
||||
- [ ] **落地/校准 `expand_query` 的提示词(按设计 §5.2)**:
|
||||
- 基础系统提示词写死在 `app/llm.py`(搬家/家居场景、列相关命名词、跟随查询语言、≤ ~8 个、不解释、不造无关词)。默认提示词起点(**可迭代** / a starting point, tune during testing):
|
||||
> 你是搬家物品搜索助手。用户在搜索自己打包的箱子与物品(家居/搬家场景)。给定一个搜索词,列出用户可能用来命名同一类物品的相关词:近义词、常见别称、上位类别、具体品类。规则:用与查询相同的语言;只给与该物品紧密相关、有助于在清单里找到它的词;不要解释、不要造无关词;最多 8 个;只输出一个 JSON 字符串数组,例如 `["炒锅","平底锅","汤锅","厨具"]`。
|
||||
- 读取 `ai_search_extra_hints`,非空则**追加**到基础提示词之后(只补充,不改格式)。
|
||||
- **输出契约**:要求模型只回 JSON 字符串数组;解析去 ` ```json ` 围栏 → `json.loads` → 失败按行/逗号兜底 → 再不行返回 `[]`;任何异常/超时都返回 `[]`(不抛错)。
|
||||
- [ ] **新增配置项 `ai_search_extra_hints`**:KV 默认空;纳入 `get_app_settings` / `save_app_settings`;设置页 `app/templates/settings/form.html` 加一个多行输入(沿用 step 2 风格)。
|
||||
- [ ] 实现检索 seam:在 `app/main.py`(或抽一个小搜索模块 `app/search.py`)加 `ai_search(db, query) -> (expanded_terms, results)`:
|
||||
- 调 `expand_query(cfg, query)` 得到扩展词;
|
||||
- 用「原词 + 扩展词」对 `name`/`note` 做 OR `LIKE`(**复用现有 `_build_search_results` 的匹配逻辑**,避免重复实现),去重。
|
||||
@@ -53,12 +63,16 @@ A **persistent** "AI search" action on the search page that broadens results via
|
||||
- [ ] 已有结果时点 AI 仍可用,且结果集被扩大(含原结果)。
|
||||
- [ ] 按钮可见性随 `ai_search_enabled` + `is_configured()` 门控。
|
||||
- [ ] 调用失败 → 回退普通结果、页面不报错。
|
||||
- [ ] `expand_query` 输出解析:模型回合法 JSON 数组 → 正确解析;回散文/坏 JSON/超时 → 返回 `[]`、不抛错。
|
||||
Output parsing: valid JSON array → parsed; prose/bad JSON/timeout → `[]`, no raise.
|
||||
- [ ] `ai_search_extra_hints` 非空时确被追加进请求(可对构造的请求体断言)。
|
||||
Extra hints, when set, are appended to the request.
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件 / Files
|
||||
|
||||
`app/main.py`、(可选 `app/search.py`)、`app/templates/search/index.html`、`tests/`。
|
||||
`app/llm.py`、`app/main.py`、(可选 `app/search.py`)、`app/templates/search/index.html`、`app/templates/settings/form.html`、配置读写 helper(step 2 的 settings store)、`tests/`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user