swarm-notes 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/PKG-INFO +10 -2
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/README.md +9 -1
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/pyproject.toml +1 -1
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/analyst.py +48 -52
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/main.py +7 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/vault_writer.py +11 -2
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/__init__.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/config.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/critic.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/discussant.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/domain_expert.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/federation.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/__init__.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/arxiv.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/base.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/biorxiv.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/common.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/openalex.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/paper_search/semantic_scholar.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/pdf_converter.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/router.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/vault_manager.py +0 -0
- {swarm_notes-0.3.2 → swarm_notes-0.3.4}/src/swarm_notes/watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: swarm-notes
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Automated research paper tracking and knowledge synthesis
|
|
5
5
|
Author: LM
|
|
6
6
|
Author-email: LM <hi@leima.is>
|
|
@@ -237,4 +237,12 @@ swarm-notes run \
|
|
|
237
237
|
|
|
238
238
|
## License
|
|
239
239
|
|
|
240
|
-
MIT — see [LICENSE](LICENSE).
|
|
240
|
+
MIT — see [LICENSE](LICENSE).
|
|
241
|
+
## Releasing
|
|
242
|
+
|
|
243
|
+
Releases are published to PyPI by `.github/workflows/release.yml` when a `v*` tag is pushed:
|
|
244
|
+
|
|
245
|
+
1. Bump `version` in `pyproject.toml` and merge to `main`.
|
|
246
|
+
2. Tag the merge commit with the same version, e.g. `git tag v0.3.4 && git push origin v0.3.4`.
|
|
247
|
+
|
|
248
|
+
The workflow checks that the tag matches `pyproject.toml`, runs the tests, builds, and publishes with PyPI trusted publishing. One-time setup: add this repository as a trusted publisher for `swarm-notes` on PyPI (workflow `release.yml`, environment `pypi`).
|
|
@@ -218,4 +218,12 @@ swarm-notes run \
|
|
|
218
218
|
|
|
219
219
|
## License
|
|
220
220
|
|
|
221
|
-
MIT — see [LICENSE](LICENSE).
|
|
221
|
+
MIT — see [LICENSE](LICENSE).
|
|
222
|
+
## Releasing
|
|
223
|
+
|
|
224
|
+
Releases are published to PyPI by `.github/workflows/release.yml` when a `v*` tag is pushed:
|
|
225
|
+
|
|
226
|
+
1. Bump `version` in `pyproject.toml` and merge to `main`.
|
|
227
|
+
2. Tag the merge commit with the same version, e.g. `git tag v0.3.4 && git push origin v0.3.4`.
|
|
228
|
+
|
|
229
|
+
The workflow checks that the tag matches `pyproject.toml`, runs the tests, builds, and publishes with PyPI trusted publishing. One-time setup: add this repository as a trusted publisher for `swarm-notes` on PyPI (workflow `release.yml`, environment `pypi`).
|
|
@@ -210,17 +210,37 @@ class PaperAnalysisBase(BaseModel):
|
|
|
210
210
|
|
|
211
211
|
class PaperAnalysis(PaperAnalysisBase):
|
|
212
212
|
"""Structured extraction output for a single academic paper."""
|
|
213
|
-
tags: list[str] = Field(
|
|
213
|
+
tags: list[str] = Field(
|
|
214
|
+
default_factory=list,
|
|
215
|
+
description=(
|
|
216
|
+
"Tags for this paper. Reuse tags from the EXISTING TAGS list whenever one applies; "
|
|
217
|
+
"only add a new tag if no existing tag describes the idea. Maximum 10 tags."
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Upper bound on how many existing tags are listed in the prompt. Tags are
|
|
223
|
+
# passed as prompt text rather than as an enum in the output schema: a schema
|
|
224
|
+
# enum with hundreds of values makes Gemini reject the request with
|
|
225
|
+
# 400 INVALID_ARGUMENT.
|
|
226
|
+
MAX_PROMPT_TAGS = 1000
|
|
214
227
|
|
|
215
228
|
|
|
216
229
|
# ---------------------------------------------------------------------------
|
|
217
230
|
# Agent factory
|
|
218
231
|
# ---------------------------------------------------------------------------
|
|
219
232
|
|
|
220
|
-
def _build_system_prompt(
|
|
233
|
+
def _build_system_prompt(
|
|
234
|
+
skill: SkillSpec, taxonomy: dict, existing_tags: list[str] | None = None
|
|
235
|
+
) -> str:
|
|
221
236
|
architectures_list = ", ".join(taxonomy.get("architectures", []))
|
|
222
237
|
domains_list = ", ".join(taxonomy.get("domains", []))
|
|
223
|
-
|
|
238
|
+
existing_tags_str = (
|
|
239
|
+
", ".join(existing_tags[:MAX_PROMPT_TAGS])
|
|
240
|
+
if existing_tags
|
|
241
|
+
else "(No tags currently exist in the vault)"
|
|
242
|
+
)
|
|
243
|
+
|
|
224
244
|
base_prompt = f"""You are an expert academic paper analyst specialising in machine learning research.
|
|
225
245
|
|
|
226
246
|
Your task is to analyse the provided paper abstract and extract structured information.
|
|
@@ -229,11 +249,14 @@ TAXONOMY (use ONLY these values for architectures/domains):
|
|
|
229
249
|
- Available architectures: {architectures_list}
|
|
230
250
|
- Available domains: {domains_list}
|
|
231
251
|
|
|
252
|
+
EXISTING TAGS IN VAULT:
|
|
253
|
+
{existing_tags_str}
|
|
254
|
+
|
|
232
255
|
SKILL CONTEXT ({skill.name}):
|
|
233
256
|
{skill.extra_system_prompt}
|
|
234
257
|
|
|
235
258
|
IMPORTANT RULES:
|
|
236
|
-
1.
|
|
259
|
+
1. Prefer tags from the EXISTING TAGS list, spelled exactly as listed. Add a new tag only when none applies, and keep new tags to an absolute minimum.
|
|
237
260
|
2. Select domain ONLY from the taxonomy domains list above.
|
|
238
261
|
3. Select architectures ONLY from the taxonomy architectures list above (or leave empty).
|
|
239
262
|
4. The summary must be 3-5 sentences, technically precise but accessible.
|
|
@@ -268,46 +291,12 @@ def analyse(paper: RawPaper, skill: SkillSpec) -> PaperAnalysis:
|
|
|
268
291
|
Structured extraction result.
|
|
269
292
|
"""
|
|
270
293
|
taxonomy = _load_taxonomy()
|
|
271
|
-
system_prompt = _build_system_prompt(skill, taxonomy)
|
|
272
294
|
existing_tags = get_existing_tags()
|
|
273
|
-
|
|
274
|
-
from pydantic import create_model
|
|
275
|
-
|
|
276
|
-
fields = {}
|
|
277
|
-
if existing_tags:
|
|
278
|
-
ExistingTags = Literal[tuple(existing_tags)] # type: ignore
|
|
279
|
-
fields["existing_tags"] = (
|
|
280
|
-
list[ExistingTags],
|
|
281
|
-
Field(
|
|
282
|
-
default_factory=list,
|
|
283
|
-
description="Tags chosen from the existing knowledge base. Pick only tags that genuinely apply."
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
fields["new_tags"] = (
|
|
287
|
-
list[str],
|
|
288
|
-
Field(
|
|
289
|
-
default_factory=list,
|
|
290
|
-
description="Use this ONLY if the concept cannot be accurately described by any of the existing_tags. Keep new tags to an absolute minimum."
|
|
291
|
-
)
|
|
292
|
-
)
|
|
293
|
-
else:
|
|
294
|
-
fields["new_tags"] = (
|
|
295
|
-
list[str],
|
|
296
|
-
Field(
|
|
297
|
-
default_factory=list,
|
|
298
|
-
description="List of new tags for this paper."
|
|
299
|
-
)
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
DynamicPaperAnalysis = create_model(
|
|
303
|
-
"DynamicPaperAnalysis",
|
|
304
|
-
__base__=PaperAnalysisBase,
|
|
305
|
-
**fields
|
|
306
|
-
)
|
|
295
|
+
system_prompt = _build_system_prompt(skill, taxonomy, existing_tags)
|
|
307
296
|
|
|
308
|
-
agent: Agent[None,
|
|
297
|
+
agent: Agent[None, PaperAnalysis] = Agent(
|
|
309
298
|
model=settings.llm_model,
|
|
310
|
-
output_type=
|
|
299
|
+
output_type=PaperAnalysis,
|
|
311
300
|
system_prompt=system_prompt,
|
|
312
301
|
)
|
|
313
302
|
|
|
@@ -323,17 +312,8 @@ def analyse(paper: RawPaper, skill: SkillSpec) -> PaperAnalysis:
|
|
|
323
312
|
logger.info("Analyst: analysing paper %s with skill %s", paper.arxiv_id, skill.name)
|
|
324
313
|
|
|
325
314
|
result = agent.run_sync(user_message)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# Merge existing and new tags
|
|
329
|
-
combined_tags = []
|
|
330
|
-
if hasattr(dynamic_analysis, "existing_tags"):
|
|
331
|
-
combined_tags.extend(getattr(dynamic_analysis, "existing_tags", []))
|
|
332
|
-
if hasattr(dynamic_analysis, "new_tags"):
|
|
333
|
-
combined_tags.extend(getattr(dynamic_analysis, "new_tags", []))
|
|
334
|
-
|
|
335
|
-
analysis_dict = dynamic_analysis.model_dump(exclude={"existing_tags", "new_tags"})
|
|
336
|
-
analysis = PaperAnalysis(**analysis_dict, tags=combined_tags)
|
|
315
|
+
analysis: PaperAnalysis = result.output
|
|
316
|
+
analysis.tags = _normalise_tags(analysis.tags, existing_tags)
|
|
337
317
|
|
|
338
318
|
# Ensure metadata from the raw paper is preserved accurately
|
|
339
319
|
analysis.arxiv_id = paper.arxiv_id
|
|
@@ -352,6 +332,22 @@ def analyse(paper: RawPaper, skill: SkillSpec) -> PaperAnalysis:
|
|
|
352
332
|
return analysis
|
|
353
333
|
|
|
354
334
|
|
|
335
|
+
def _normalise_tags(tags: list[str], existing_tags: list[str]) -> list[str]:
|
|
336
|
+
"""Map tags onto the vault's existing spelling (case-insensitive) and drop duplicates."""
|
|
337
|
+
canonical = {t.lower(): t for t in existing_tags}
|
|
338
|
+
result: list[str] = []
|
|
339
|
+
seen: set[str] = set()
|
|
340
|
+
for tag in tags:
|
|
341
|
+
tag = tag.strip()
|
|
342
|
+
if not tag:
|
|
343
|
+
continue
|
|
344
|
+
tag = canonical.get(tag.lower(), tag)
|
|
345
|
+
if tag.lower() not in seen:
|
|
346
|
+
seen.add(tag.lower())
|
|
347
|
+
result.append(tag)
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
|
|
355
351
|
def _load_taxonomy() -> dict:
|
|
356
352
|
"""Load taxonomy.json, returning an empty dict on failure."""
|
|
357
353
|
if not settings.taxonomy_file.exists():
|
|
@@ -328,6 +328,13 @@ def run(
|
|
|
328
328
|
len(papers),
|
|
329
329
|
)
|
|
330
330
|
|
|
331
|
+
if not analyses:
|
|
332
|
+
# Every paper failed (e.g. the LLM rejected every request). Exit
|
|
333
|
+
# non-zero so the scheduled workflow shows up as failed instead of
|
|
334
|
+
# silently committing nothing.
|
|
335
|
+
logger.error("No papers were processed successfully – failing the run")
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
|
|
331
338
|
except typer.Exit:
|
|
332
339
|
# Re-raise Typer exit as-is
|
|
333
340
|
raise
|
|
@@ -616,5 +616,14 @@ def _slugify(text: str) -> str:
|
|
|
616
616
|
|
|
617
617
|
|
|
618
618
|
def _yaml_escape(text: str) -> str:
|
|
619
|
-
"""Escape
|
|
620
|
-
|
|
619
|
+
"""Escape text for use inside a double-quoted YAML scalar.
|
|
620
|
+
|
|
621
|
+
Backslashes must be escaped first: titles with LaTeX such as
|
|
622
|
+
``$\\mathrm{SO}(2)$`` otherwise produce invalid escape sequences and
|
|
623
|
+
break every YAML parser that reads the note (including the site build).
|
|
624
|
+
"""
|
|
625
|
+
return (
|
|
626
|
+
text.replace("\\", "\\\\")
|
|
627
|
+
.replace('"', '\\"')
|
|
628
|
+
.replace("\n", " ")
|
|
629
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|