Skip to content

Commit 6ce3cb9

Browse files
committed
feat: streamline Obsidian Zotero review workflow
- replace the import-only wrapper with a dedicated review-agent CLI entrypoint\n- inject session model config into Obsidian review tools and normalize vault metadata\n- surface Obsidian review results in the frontend and document the unified workflow
1 parent 9df9707 commit 6ce3cb9

File tree

10 files changed

+1216
-300
lines changed

10 files changed

+1216
-300
lines changed

ScienceClaw/backend/deepagent/agent.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"""
2424
from __future__ import annotations
2525

26+
import json
2627
import os
2728
from datetime import datetime
2829
from typing import Any, Dict, List, Optional, Set, Tuple
@@ -80,6 +81,10 @@
8081
"obsidian_run_zotero_review_agent",
8182
"obsidian_rewrite_materials_review_note",
8283
}
84+
_OBSIDIAN_MODEL_CONFIG_TOOL_NAMES = {
85+
"obsidian_run_zotero_review_agent",
86+
"obsidian_rewrite_materials_review_note",
87+
}
8388

8489
# ───────────────────────────────────────────────────────────────────
8590
# Backend 构建
@@ -329,7 +334,51 @@ async def _wrapped_coroutine(*args: Any, **kwargs: Any) -> Any:
329334
return tool_obj.model_copy(update=updates)
330335

331336

332-
def _collect_tools(blocked_tools: Set[str] | None = None, preferred_vault: str = "") -> List:
337+
def _inject_default_model_config(tool_obj: Any, preferred_model_config: Optional[Dict[str, Any]]) -> Any:
338+
if getattr(tool_obj, "name", "") not in _OBSIDIAN_MODEL_CONFIG_TOOL_NAMES:
339+
return tool_obj
340+
if not isinstance(tool_obj, StructuredTool):
341+
return tool_obj
342+
if not isinstance(preferred_model_config, dict) or not preferred_model_config:
343+
return tool_obj
344+
345+
base_func = getattr(tool_obj, "func", None)
346+
if base_func is None:
347+
return tool_obj
348+
349+
serialized = json.dumps(preferred_model_config, ensure_ascii=False, default=str)
350+
351+
def _wrapped_func(*args: Any, **kwargs: Any) -> Any:
352+
actual_kwargs = dict(kwargs)
353+
if not str(actual_kwargs.get("model_config_json", "") or "").strip():
354+
actual_kwargs["model_config_json"] = serialized
355+
return base_func(*args, **actual_kwargs)
356+
357+
updates: Dict[str, Any] = {"func": _wrapped_func}
358+
base_coroutine = getattr(tool_obj, "coroutine", None)
359+
if base_coroutine is not None:
360+
async def _wrapped_coroutine(*args: Any, **kwargs: Any) -> Any:
361+
actual_kwargs = dict(kwargs)
362+
if not str(actual_kwargs.get("model_config_json", "") or "").strip():
363+
actual_kwargs["model_config_json"] = serialized
364+
return await base_coroutine(*args, **actual_kwargs)
365+
366+
updates["coroutine"] = _wrapped_coroutine
367+
368+
logger.info(
369+
"[Agent] Injecting session model config into tool {}: model={} id={}",
370+
tool_obj.name,
371+
str(preferred_model_config.get("model_name", "")).strip() or "unknown",
372+
str(preferred_model_config.get("id", "")).strip() or "n/a",
373+
)
374+
return tool_obj.model_copy(update=updates)
375+
376+
377+
def _collect_tools(
378+
blocked_tools: Set[str] | None = None,
379+
preferred_vault: str = "",
380+
preferred_model_config: Optional[Dict[str, Any]] = None,
381+
) -> List:
333382
"""合并内置工具与外部扩展工具,去重并过滤屏蔽项。
334383
335384
通过 DirWatcher 检测 Tools/ 目录变更,仅在变更时才重新 import 模块。
@@ -350,6 +399,7 @@ def _collect_tools(blocked_tools: Set[str] | None = None, preferred_vault: str =
350399
logger.info(f"[Agent] 工具已屏蔽,跳过: {t.name}")
351400
continue
352401
t = _inject_default_vault_dir(t, preferred_vault)
402+
t = _inject_default_model_config(t, preferred_model_config)
353403
if t.name not in seen_names:
354404
all_tools.append(t)
355405
seen_names.add(t.name)
@@ -433,7 +483,11 @@ async def deep_agent(
433483
# ── 检测 Tools/Skills 目录变更并按需重新加载 ──
434484
_dir_watcher.has_changed(_EXTERNAL_SKILLS_DIR)
435485

436-
tools = _collect_tools(blocked_tools=blocked_tools, preferred_vault=ts.obsidian_vault_dir)
486+
tools = _collect_tools(
487+
blocked_tools=blocked_tools,
488+
preferred_vault=ts.obsidian_vault_dir,
489+
preferred_model_config=model_config,
490+
)
437491

438492
sse_middleware = SSEMonitoringMiddleware(
439493
agent_name="DeepAgent",

ScienceClaw/backend/scripts/import_zotero_bbt_to_obsidian.py renamed to ScienceClaw/backend/scripts/run_zotero_review_agent.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
r"""Import a Better BibTeX JSON export into the Materials subtree of an Obsidian vault.
1+
r"""Run the Zotero Review Agent end-to-end and write results into an Obsidian vault.
22
33
Usage from the repo root:
4-
python .\ScienceClaw\backend\scripts\import_zotero_bbt_to_obsidian.py ^
4+
python .\ScienceClaw\backend\scripts\run_zotero_review_agent.py ^
55
--input .\zotero\生成模型.json ^
6-
--topic 生成模型综述 ^
7-
--vault D:/Obsidian/MyVault
6+
--topic 生成模型 ^
7+
--vault D:/Obsidian/MyVault ^
8+
--overwrite
89
"""
910
from __future__ import annotations
1011

@@ -49,49 +50,75 @@ def tool(fn):
4950
sys.modules["langchain_core.tools"] = tools_module
5051

5152

53+
def _find_repo_root() -> Path:
54+
current = Path(__file__).resolve()
55+
for candidate in [current.parent, *current.parents]:
56+
tool_path = candidate / "Tools" / "obsidian_run_zotero_review_agent.py"
57+
if tool_path.exists():
58+
return candidate
59+
raise RuntimeError(f"Unable to locate repo root from {current}")
60+
61+
5262
def _load_tool():
5363
_install_langchain_stub()
5464

55-
repo_root = Path(__file__).resolve().parents[3]
56-
tool_path = repo_root / "Tools" / "obsidian_import_zotero_bbt_json.py"
57-
spec = importlib.util.spec_from_file_location("obsidian_import_zotero_bbt_json_module", tool_path)
65+
repo_root = _find_repo_root()
66+
tool_path = repo_root / "Tools" / "obsidian_run_zotero_review_agent.py"
67+
spec = importlib.util.spec_from_file_location("obsidian_run_zotero_review_agent_module", tool_path)
5868
if spec is None or spec.loader is None:
5969
raise RuntimeError(f"Unable to load tool from {tool_path}")
6070

6171
module = importlib.util.module_from_spec(spec)
6272
spec.loader.exec_module(module)
63-
tool_obj = getattr(module, "obsidian_import_zotero_bbt_json", None)
73+
tool_obj = getattr(module, "obsidian_run_zotero_review_agent", None)
6474
if tool_obj is None or not hasattr(tool_obj, "invoke"):
65-
raise RuntimeError("obsidian_import_zotero_bbt_json tool is unavailable")
75+
raise RuntimeError("obsidian_run_zotero_review_agent tool is unavailable")
6676
return tool_obj
6777

6878

79+
def _load_model_config(args: argparse.Namespace) -> str:
80+
inline_value = str(args.model_config_json or "").strip()
81+
file_value = str(args.model_config_file or "").strip()
82+
if inline_value and file_value:
83+
raise ValueError("Use only one of --model-config-json or --model-config-file")
84+
if file_value:
85+
config_path = Path(file_value).expanduser()
86+
if not config_path.is_absolute():
87+
config_path = Path.cwd() / config_path
88+
return config_path.read_text(encoding="utf-8")
89+
return inline_value
90+
91+
6992
def main() -> int:
7093
parser = argparse.ArgumentParser(description=__doc__)
7194
parser.add_argument("--input", required=True, help="Path to Better BibTeX JSON export")
7295
parser.add_argument("--topic", default="", help="Review topic name; defaults to JSON filename stem")
96+
parser.add_argument("--category", default="", help="Category folder name; defaults to topic")
7397
parser.add_argument("--vault", default="", help="Existing Obsidian vault path to mount as OBSIDIAN_VAULT_DIR")
74-
parser.add_argument("--max-items", type=int, default=0, help="Max number of parent items to import; 0 = all")
75-
parser.add_argument("--alloy-family", default="", help="Optional alloy_family frontmatter value")
76-
parser.add_argument("--property-focus", default="", help="Optional property_focus frontmatter value")
98+
parser.add_argument("--max-items", type=int, default=0, help="Max number of parent items to process; 0 = all")
7799
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing literature/review notes")
78-
parser.add_argument("--no-review", action="store_true", help="Skip creating the review note")
100+
parser.add_argument("--model-config-json", default="", help="Optional serialized model config JSON for the final polish pass")
101+
parser.add_argument("--model-config-file", default="", help="Optional UTF-8 JSON file containing the model config")
79102
args = parser.parse_args()
80103

81104
if args.vault:
82105
os.environ["OBSIDIAN_VAULT_DIR"] = args.vault
83106

107+
try:
108+
model_config_json = _load_model_config(args)
109+
except (OSError, ValueError) as exc:
110+
parser.error(str(exc))
111+
84112
tool_obj = _load_tool()
85113
result = tool_obj.invoke(
86114
{
87115
"export_json_path": args.input,
88116
"topic": args.topic,
89-
"max_items": args.max_items,
90-
"alloy_family": args.alloy_family,
91-
"property_focus": args.property_focus,
117+
"category": args.category,
92118
"vault_dir": args.vault,
93119
"overwrite_existing": args.overwrite,
94-
"create_review_note": not args.no_review,
120+
"max_items": args.max_items,
121+
"model_config_json": model_config_json,
95122
}
96123
)
97124
print(json.dumps(result, ensure_ascii=False, indent=2))

ScienceClaw/frontend/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
<body>
1313
<div id="app"></div>
1414
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
15-
<script src="/libs/openchemlib-full.js"></script>
15+
<script type="module" src="/libs/openchemlib-global.js"></script>
1616
<script src="/libs/3Dmol-min.js"></script>
1717
<script type="module" src="/src/main.ts"></script>
1818
</body>
1919

20-
</html>
20+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import OCL from './openchemlib-full.js';
2+
3+
if (typeof window !== 'undefined' && !window.OCL) {
4+
window.OCL = OCL;
5+
}

ScienceClaw/frontend/src/components/ToolPanelContent.vue

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@
7272
<!-- 结果 -->
7373
<div v-if="toolContent.content != null">
7474
<div class="text-[11px] text-[var(--text-tertiary)] mb-1.5 uppercase tracking-wider font-medium">Output</div>
75+
<div
76+
v-if="obsidianSummary"
77+
class="mb-3 rounded-xl border border-emerald-200/70 bg-emerald-50/70 px-3 py-3 dark:border-emerald-900/60 dark:bg-emerald-950/20"
78+
>
79+
<div class="text-[11px] font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-300">
80+
Obsidian Result
81+
</div>
82+
<div v-if="obsidianSummary.topic" class="mt-2">
83+
<div class="text-[11px] text-[var(--text-tertiary)]">Topic</div>
84+
<div class="text-[12px] leading-relaxed break-words text-[var(--text-primary)]">{{ obsidianSummary.topic }}</div>
85+
</div>
86+
<div v-if="obsidianSummary.reviewNotePath" class="mt-2">
87+
<div class="text-[11px] text-[var(--text-tertiary)]">Saved note</div>
88+
<code class="block text-[12px] leading-relaxed break-all text-[var(--text-primary)]">{{ obsidianSummary.reviewNotePath }}</code>
89+
</div>
90+
<div v-if="obsidianSummary.effectiveVaultDir" class="mt-2">
91+
<div class="text-[11px] text-[var(--text-tertiary)]">Vault</div>
92+
<code class="block text-[12px] leading-relaxed break-all text-[var(--text-primary)]">{{ obsidianSummary.effectiveVaultDir }}</code>
93+
</div>
94+
<div v-if="obsidianSummary.reviewInputPath" class="mt-2">
95+
<div class="text-[11px] text-[var(--text-tertiary)]">Review input</div>
96+
<code class="block text-[12px] leading-relaxed break-all text-[var(--text-primary)]">{{ obsidianSummary.reviewInputPath }}</code>
97+
</div>
98+
<div v-if="obsidianSummary.reviewDraftPath" class="mt-2">
99+
<div class="text-[11px] text-[var(--text-tertiary)]">Review draft</div>
100+
<code class="block text-[12px] leading-relaxed break-all text-[var(--text-primary)]">{{ obsidianSummary.reviewDraftPath }}</code>
101+
</div>
102+
</div>
75103
<pre class="text-[12px] leading-relaxed whitespace-pre-wrap break-words text-[var(--text-secondary)] bg-[var(--fill-tsp-gray-main)] rounded-lg px-3 py-2 border border-[var(--border-light)] max-h-[400px] overflow-y-auto">{{ contentJson }}</pre>
76104
</div>
77105
<!-- 空状态 -->
@@ -152,6 +180,33 @@ const safeStringify = (value: any): string => {
152180
}
153181
};
154182
183+
const obsidianSummary = computed(() => {
184+
const fn = props.toolContent?.function || props.toolContent?.name || '';
185+
if (!fn.startsWith('obsidian_')) return null;
186+
187+
const content = props.toolContent?.content;
188+
if (!content || typeof content !== 'object' || Array.isArray(content)) return null;
189+
190+
const data = content as Record<string, unknown>;
191+
const topic = String(data.topic || '').trim();
192+
const reviewNotePath = String(data.review_note_path || data.note_path || data.relative_note_path || '').trim();
193+
const effectiveVaultDir = String(data.effective_vault_dir || data.vault_dir || '').trim();
194+
const reviewInputPath = String(data.review_input_path || '').trim();
195+
const reviewDraftPath = String(data.review_draft_path || '').trim();
196+
197+
if (!topic && !reviewNotePath && !effectiveVaultDir && !reviewInputPath && !reviewDraftPath) {
198+
return null;
199+
}
200+
201+
return {
202+
topic,
203+
reviewNotePath,
204+
effectiveVaultDir,
205+
reviewInputPath,
206+
reviewDraftPath,
207+
};
208+
});
209+
155210
const argsJson = computed(() => safeStringify(props.toolContent?.args));
156211
const contentJson = computed(() => safeStringify(props.toolContent?.content));
157212

0 commit comments

Comments
 (0)