algonotes_rag/scripts/update.py

207 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# scripts/update.py
# CLI backend: update existing notes in three-layer storage.
#
# Supports:
# - Local note update (requires --file)
# - URL note re-crawl (from stored source_url)
import argparse
import json
import time
from dataclasses import dataclass
from pathlib import Path
from langchain_core.documents import Document
from src.store.file_store import get_file_store
from src.store.sql_store import get_sql_store
from src.store.vector_store import get_vector_store
from src.logger import setup_logger
from src.ingestion.cleaner import clean_text
from src.ingestion.splitter import split_markdown
from src.ingestion.tagger import extract_tags
_file_store = get_file_store()
_sql_store = get_sql_store()
_vector_store = get_vector_store()
logger = setup_logger("algonotes.cli.update")
@dataclass
class UpdateResult:
"""Result of a note update."""
file_name: str
chunk_count: int
tags: str
title: str
note_id: int
success: bool
@dataclass
class MetadataResult:
"""Result of a metadata update."""
filename: str
success: bool
fields_updated: list[str]
def update_metadata(filename: str, title: str | None = None,
tags: str | None = None, author: str | None = None,
type: str | None = None) -> MetadataResult:
"""Update note metadata without reprocessing content.
Args:
filename: The note filename to update.
title: New title.
tags: New tags (comma-separated).
author: New author.
type: New note type.
Returns:
A MetadataResult indicating success and updated fields.
"""
record = _sql_store.get_by_filename(filename)
if not record:
return MetadataResult(filename, False, [])
_sql_store.update_metadata(
record["id"], title=title, tags=tags, author=author, type=type)
vec_fields = {}
if tags is not None:
vec_fields["tags"] = tags
if vec_fields:
_vector_store.update_metadata_fields(filename, vec_fields)
updated = [k for k, v in {"title": title, "tags": tags,
"author": author, "type": type}.items()
if v is not None]
return MetadataResult(filename, True, updated)
def update_note(file_name: str, file_path: Path | None = None,
tag: bool = True,
verbose: bool = False, type: str | None = None,
author: str | None = None) -> UpdateResult:
"""Update a note across all three storage layers.
For local notes: provide file_path to update content.
For URL notes: omit file_path to re-crawl from stored URL.
Args:
file_name: The note filename to update (e.g. "fenwick.md").
file_path: Path to new local file (optional).
tag: Extract tags via LLM.
verbose: Print processing details.
type: Note type (note/solution/template).
author: Note author.
Returns:
An UpdateResult indicating success and chunk count.
"""
sql_record = _sql_store.get_by_filename(file_name)
if not sql_record:
logger.warning(f"Note not found: {file_name}")
return UpdateResult(file_name, 0, "", "", 0, False)
source_url = sql_record.get("source_url")
if file_path:
content = Path(file_path).read_text(encoding="utf-8")
elif source_url:
if verbose:
print(" 清洗: ")
print(" > ", end="")
from langchain_community.document_loaders import WebBaseLoader
from src.ingestion.loader import _HEADERS
docs = WebBaseLoader(source_url, header_template=_HEADERS).load()
content = docs[0].page_content
content = clean_text(content, verbose)
_file_store.update(file_name, content)
else:
logger.error(f"Cannot update {file_name}: no --file and no source_url")
return UpdateResult(file_name, 0, "", "", 0, False)
new_filepath = str(_file_store._resolve(file_name).resolve())
_vector_store.delete(file_name)
chunks: list[Document] = split_markdown(content)
tags_str = ""
if tag:
tags_str = extract_tags(file_name, _file_store)
title = chunks[0].metadata.get("header_h1", file_name)
if verbose:
print(f" 标题: {title}")
print(f" 分块: {len(chunks)}")
print(f" 打 tag: {tags_str}")
_sql_store.update(
sql_record["id"],
filepath=new_filepath,
title=title,
tags=tags_str,
type=type,
author=author,
chunk_count=len(chunks),
)
for chunk in chunks:
chunk.metadata["tags"] = tags_str
doc_ids: list[str] = _vector_store.save(file_name, chunks)
if verbose:
print(f" 向量: {len(doc_ids)} chunks")
logger.info(f"Updated {file_name} ({len(chunks)} chunks)")
return UpdateResult(file_name, len(chunks), tags_str, title, sql_record["id"], True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="algonotes update",
description="更新已有笔记的内容。",
)
parser.add_argument("filename", help="笔记文件名")
parser.add_argument("--file", type=Path, help="新的本地文件路径")
parser.add_argument("--no-tag", action="store_true", help="跳过打 tag")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
parser.add_argument("--json", type=Path, metavar="FILE",
help="输出结果到 JSON 文件")
args = parser.parse_args()
tag = not args.no_tag
sql_record = _sql_store.get_by_filename(args.filename)
if not sql_record:
print(f"❌ 笔记不存在: {args.filename}")
exit(1)
if not args.file and not sql_record.get("source_url"):
print(f"❌ 笔记 {args.filename} 无 source_url请使用 --file 指定新文件")
exit(1)
result = update_note(
args.filename,
file_path=args.file,
tag=tag,
verbose=args.verbose,
)
if result.success:
print(f"Update success: {result.file_name} ({result.chunk_count} chunks)")
else:
print(f"Update failed: {result.file_name}")
if args.json:
args.json.write_text(
json.dumps(result.__dict__, ensure_ascii=False, indent=2),
encoding="utf-8",
)