Anki 24.11 (87ccd24e) (ao)
Python 3.9.18 Qt 6.6.2 PyQt 6.6.1
Platform: macOS-12.7.6-arm64-arm-64bit
Traceback (most recent call last):
File "aqt.taskman", line 144, in _on_closures_pending
File "aqt.taskman", line 88, in <lambda>
File "aqt.taskman", line 108, in wrapped_done
File "aqt.operations", line 130, in wrapped_done
File "aqt.operations", line 159, in on_op_finished
File "_aqt.hooks", line 3840, in __call__
File "aqt.main", line 845, in on_operation_did_execute
File "aqt.deckbrowser", line 94, in op_executed
AttributeError: 'function' object has no attribute 'study_queues'
I wrote a plugin with Gemini 2.0 that uses AI to generate readings based on what I’ve learned today, and it generates the readings correctly, but after generating them there is always the error reported above that comes up. I’ve had the AI fix it several times, but it’s not getting any better. Here are the contents of the py file.
import json
from aqt import mw
from aqt.qt import *
from aqt.utils import showInfo, showText, checkInvalidFilename
from aqt.webview import AnkiWebView
import os
from anki.hooks import addHook
import requests
from typing import Any, Optional, Tuple, Union
# --- Compatibility Import ---
try:
# Newer Anki versions (23.10+)
from aqt.operations import QueryOp
from aqt.operations import CollectionOp
except ImportError:
# Older Anki versions
from anki.scheduler.base import CollectionOp as co
# --- Result Wrapper ---
class SimpleResult:
def __init__(self, result: Optional[dict] = None):
self.result = result
# Indicate changes if result contains "result" key
self._changes = "result" in (result or {})
def changes(self) -> bool:
return self._changes
# --- 配置相关 ---
config_file = os.path.join(os.path.dirname(__file__), "config.json")
default_config = {
"api_token": "YOUR_DEFAULT_API_TOKEN",
"request_url": "YOUR_DEFAULT_REQUEST_URL",
"model_name": "YOUR_DEFAULT_MODEL_NAME",
"max_tokens": 4096,
"field_name": "VocabKanji",
"temperature": 0.7,
"top_p": 0.7,
"top_k": 50,
"frequency_penalty": 0.5,
"stop_sequences": [],
"enable_cache": True,
"prompt_template": "你是一名日本の小説家,请根据日语单词生成一篇小说,必须用日语进行回答,只生成简短的小说,不产生其他内容:\n\n{vocab_list}",
"note_type_name": "Basic",
"target_field_name": "Reading",
"add_to_card": True, # Option to add the generated article to a card
}
def load_config():
try:
with open(config_file, "r") as f:
config = json.load(f)
# 填充默认值
for key, value in default_config.items():
if key not in config:
config[key] = value
return config
except FileNotFoundError:
return default_config
def save_config(config):
with open(config_file, "w") as f:
json.dump(config, f, indent=4)
# --- 缓存 ---
cache = {}
def load_cache():
global cache
cache_file = os.path.join(os.path.dirname(__file__), "cache.json")
try:
with open(cache_file, "r") as f:
cache = json.load(f)
except FileNotFoundError:
cache = {}
def save_cache():
cache_file = os.path.join(os.path.dirname(__file__), "cache.json")
with open(cache_file, "w") as f:
json.dump(cache, f, indent=4)
# --- 配置界面 ---
class ConfigDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("AI 服务配置")
self.setMinimumWidth(500)
form_layout = QFormLayout()
self.api_token_edit = QLineEdit()
form_layout.addRow("API 密钥:", self.api_token_edit)
self.request_url_edit = QLineEdit()
form_layout.addRow("请求 URL:", self.request_url_edit)
self.model_name_edit = QLineEdit()
form_layout.addRow("模型名称:", self.model_name_edit)
self.max_tokens_edit = QSpinBox()
self.max_tokens_edit.setRange(1, 100000)
self.max_tokens_edit.setSingleStep(100)
form_layout.addRow("最大 Token 数:", self.max_tokens_edit)
self.field_name_edit = QLineEdit()
form_layout.addRow("单词字段名:", self.field_name_edit)
self.temperature_edit = QDoubleSpinBox()
self.temperature_edit.setRange(0.0, 1.0)
self.temperature_edit.setSingleStep(0.1)
form_layout.addRow("Temperature:", self.temperature_edit)
self.top_p_edit = QDoubleSpinBox()
self.top_p_edit.setRange(0.0, 1.0)
self.top_p_edit.setSingleStep(0.1)
form_layout.addRow("Top P:", self.top_p_edit)
self.top_k_edit = QSpinBox()
self.top_k_edit.setRange(0, 100)
self.top_k_edit.setSingleStep(1)
form_layout.addRow("Top K:", self.top_k_edit)
self.frequency_penalty_edit = QDoubleSpinBox()
self.frequency_penalty_edit.setRange(0.0, 2.0)
self.frequency_penalty_edit.setSingleStep(0.1)
form_layout.addRow("Frequency Penalty:", self.frequency_penalty_edit)
self.stop_sequences_edit = QLineEdit()
form_layout.addRow("Stop Sequences (comma-separated):", self.stop_sequences_edit)
self.enable_cache_checkbox = QCheckBox("启用缓存")
form_layout.addRow(self.enable_cache_checkbox)
self.prompt_template_edit = QPlainTextEdit()
form_layout.addRow("Prompt Template:", self.prompt_template_edit)
self.note_type_name_edit = QLineEdit()
form_layout.addRow("笔记类型名称:", self.note_type_name_edit)
self.target_field_name_edit = QLineEdit()
form_layout.addRow("目标字段名称:", self.target_field_name_edit)
self.add_to_card_checkbox = QCheckBox("将生成的文章添加到卡片")
form_layout.addRow(self.add_to_card_checkbox)
self.load_from_config()
# 兼容不同版本的 PyQt
if hasattr(QDialogButtonBox, 'StandardButton'):
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
else:
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
main_layout = QVBoxLayout()
main_layout.addLayout(form_layout)
main_layout.addWidget(button_box)
self.setLayout(main_layout)
def load_from_config(self):
config = load_config()
self.api_token_edit.setText(config["api_token"])
self.request_url_edit.setText(config["request_url"])
self.model_name_edit.setText(config["model_name"])
self.max_tokens_edit.setValue(config["max_tokens"])
self.field_name_edit.setText(config["field_name"])
self.temperature_edit.setValue(config["temperature"])
self.top_p_edit.setValue(config["top_p"])
self.top_k_edit.setValue(config["top_k"])
self.frequency_penalty_edit.setValue(config["frequency_penalty"])
self.stop_sequences_edit.setText(", ".join(config["stop_sequences"]))
self.enable_cache_checkbox.setChecked(config["enable_cache"])
self.prompt_template_edit.setPlainText(config["prompt_template"])
self.note_type_name_edit.setText(config["note_type_name"])
self.target_field_name_edit.setText(config["target_field_name"])
self.add_to_card_checkbox.setChecked(config["add_to_card"])
def save_to_config(self):
config = {
"api_token": self.api_token_edit.text(),
"request_url": self.request_url_edit.text(),
"model_name": self.model_name_edit.text(),
"max_tokens": self.max_tokens_edit.value(),
"field_name": self.field_name_edit.text(),
"temperature": self.temperature_edit.value(),
"top_p": self.top_p_edit.value(),
"top_k": self.top_k_edit.value(),
"frequency_penalty": self.frequency_penalty_edit.value(),
"stop_sequences": [seq.strip() for seq in self.stop_sequences_edit.text().split(",") if seq.strip()],
"enable_cache": self.enable_cache_checkbox.isChecked(),
"prompt_template": self.prompt_template_edit.toPlainText(),
"note_type_name": self.note_type_name_edit.text(),
"target_field_name": self.target_field_name_edit.text(),
"add_to_card": self.add_to_card_checkbox.isChecked(),
}
save_config(config)
def accept(self):
self.save_to_config()
super().accept()
def show_config_dialog():
dialog = ConfigDialog(mw)
dialog.exec()
# --- Anki 主程序 ---
def get_today_learned_cards_field(field_name):
cids = mw.col.find_cards("rated:1")
field_values = []
for cid in cids:
card = mw.col.get_card(cid)
note = card.note()
if field_name in note:
field_values.append(note[field_name])
else:
return None
return field_values
def generate_review_article_with_ai(vocab_list: list[str]) -> SimpleResult:
"""
Generates a review article using AI.
Args:
vocab_list: A list of vocabulary words.
Returns:
A SimpleResult object containing the result.
"""
config = load_config()
api_token = config["api_token"]
request_url = config["request_url"]
model_name = config["model_name"]
max_tokens = config["max_tokens"]
temperature = config["temperature"]
top_p = config["top_p"]
top_k = config["top_k"]
frequency_penalty = config["frequency_penalty"]
stop_sequences = config["stop_sequences"]
enable_cache = config["enable_cache"]
vocab_str = ', '.join(vocab_list)
prompt = config["prompt_template"].format(vocab_list=vocab_str)
if enable_cache:
load_cache()
if vocab_str in cache:
print("Using cached response")
return SimpleResult({"result": cache[vocab_str]})
payload = {
"model": model_name,
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": prompt}],
}
],
"stream": False,
"max_tokens": max_tokens,
"temperature": temperature,
"top_p": top_p,
"top_k": top_k,
"frequency_penalty": frequency_penalty,
"stop": stop_sequences,
"n": 1,
"response_format": {"type": "text"},
}
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
}
try:
response = requests.post(request_url, headers=headers, json=payload)
response.raise_for_status()
response_json = response.json()
print(f"API 响应的完整 JSON: {response_json}")
if 'choices' in response_json and response_json['choices']:
generated_text = response_json['choices'][0]['message']['content']
if enable_cache:
cache[vocab_str] = generated_text
save_cache()
return SimpleResult({"result": generated_text})
else:
err_msg = "无法从 API 响应中提取文本。请检查响应结构。"
return SimpleResult({"error": err_msg})
except requests.exceptions.RequestException as e:
err_msg = f"调用 AI 服务 API 出错: {e}"
return SimpleResult({"error": err_msg})
except (KeyError, IndexError) as e:
err_msg = f"解析 AI 服务 API 响应出错: {e}"
return SimpleResult({"error": err_msg})
except json.JSONDecodeError as e:
err_msg = f"JSON 解析错误: {e}"
return SimpleResult({"error": err_msg})
def add_article_to_note(article):
config = load_config()
note_type_name = config["note_type_name"]
target_field_name = config["target_field_name"]
# Find the note type
model = mw.col.models.byName(note_type_name)
if not model:
showInfo(f"Note type '{note_type_name}' not found.")
return False
# Create a new note
note = mw.col.newNote(model['id'])
note[target_field_name] = article
# Add the note to the collection
mw.col.addNote(note)
return True # Indicate that the note was added
def on_success(simple_result: SimpleResult):
if simple_result.result:
if "result" in simple_result.result:
article = simple_result.result["result"]
showInfo(f"Generated review article:\n\n{article}")
config = load_config()
if config["add_to_card"] and config["note_type_name"] and config["target_field_name"]:
if not mw.col.models.byName(config["note_type_name"]):
showInfo(f"Note type '{config['note_type_name']}' not found. Skipping adding article to note.")
elif config["target_field_name"] not in mw.col.models.fieldNames(mw.col.models.byName(config["note_type_name"])):
showInfo(f"Field '{config['target_field_name']}' not found in note type '{config['note_type_name']}'. Skipping adding article to note.")
else:
ret = showInfo("Add generated article to a new note?", type="yesno")
if ret == QMessageBox.Yes:
# Check if add_article_to_note was successful
success = add_article_to_note(article)
if success:
mw.reset()
elif "error" in simple_result.result:
# Handle the error case
showText(f"Error: {simple_result.result['error']}") # Display the error message
else:
showText("An unknown error occurred.")
def on_failure(exception):
showText(f"Error: {exception}")
def generate_review_article_op(vocab_list) -> CollectionOp[SimpleResult]:
return CollectionOp(
parent=mw,
op=lambda col: generate_review_article_with_ai(vocab_list)
)
def test():
config = load_config()
vocab_kanji_values = get_today_learned_cards_field(config["field_name"])
if vocab_kanji_values:
showInfo("Generating article... Please wait.")
op = generate_review_article_op(vocab_kanji_values)
op.success(on_success).failure(on_failure).run_in_background()
else:
showInfo(f"No {config['field_name']} field found or no cards learned today.")
# --- 菜单项 ---
action = QAction("AI Reading Gen:生成文章", mw)
qconnect(action.triggered, test)
mw.form.menuTools.addAction(action)
# 添加配置菜单项
config_action = QAction("AI Reading Gen:AI 服务配置", mw)
qconnect(config_action.triggered, show_config_dialog)
mw.form.menuTools.addAction(config_action)