diff --git a/.agents/skills/hugo-event-intake/scripts/hugo_event_intake.py b/.agents/skills/hugo-event-intake/scripts/hugo_event_intake.py index d183af856f..744a64f753 100644 --- a/.agents/skills/hugo-event-intake/scripts/hugo_event_intake.py +++ b/.agents/skills/hugo-event-intake/scripts/hugo_event_intake.py @@ -253,14 +253,25 @@ def infer_kind( "論文読み会", "読み会", "輪読", + "論文", + "paper", + "arxiv.org", + "transformer-circuits.pub", + "survey on", + "a survey", + "サーベイ論文", + "提案について紹介", ) ): return "Journal Club" if "poster" in haystack: return "Invited Poster" - if "presentation" in haystack: + if any( + keyword in haystack + for keyword in ("invited presentation", "doctoral dissertation presentation") + ): return "Invited Presentation" - if any(keyword in haystack for keyword in ("report", "速報", "summary", "survey")): + if any(keyword in haystack for keyword in ("report", "速報", "summary", "参加報告")): return "Report" return "Invited Talk" @@ -529,6 +540,21 @@ def derive_slug( return "event-entry" +def link_label_for_event_url(url: str) -> str: + host = urlsplit(url).netloc.lower() + if host.endswith("connpass.com"): + return "Connpass" + if host.endswith("confit.atlas.jp"): + return "Confit" + return "Event" + + +def default_links_for_event_url(url: str) -> list[dict[str, str]]: + if not url: + return [] + return [{"name": link_label_for_event_url(url), "url": url}] + + def build_body_markdown( spec: EventSpec, *, description: str, speakerdeck_source: SourceInfo | None ) -> str: @@ -551,19 +577,27 @@ def build_body_markdown( return "\n".join(part.rstrip() for part in parts if part.strip()) + "\n" links = [] + seen_urls: set[str] = set() + + def add_link(name: str, url: str) -> None: + if not url or url in seen_urls: + return + links.append((name, url)) + seen_urls.add(url) + if spec.event_url: - links.append(("Event", spec.event_url)) + add_link(link_label_for_event_url(spec.event_url), spec.event_url) if spec.url_slides: - links.append(("Slides", spec.url_slides)) + add_link("Slides", spec.url_slides) if spec.url_pdf: - links.append(("PDF", spec.url_pdf)) + add_link("PDF", spec.url_pdf) if spec.url_video: - links.append(("Video", spec.url_video)) + add_link("Video", spec.url_video) if spec.url_code: - links.append(("Code", spec.url_code)) + add_link("Code", spec.url_code) if spec.links: for link in spec.links: - links.append((link["name"], link["url"])) + add_link(link["name"], link["url"]) if links: parts.append( "## Links\n\n" @@ -752,7 +786,7 @@ def probe_event( url_pdf=final_url_pdf, url_video=final_url_video, url_code=final_url_code, - links=[], + links=default_links_for_event_url(final_event_url), body_markdown="", featured_source_url=final_featured_source, evidence_urls=[source.url for source in sources], diff --git a/.agents/skills/hugo-talks-sync/SKILL.md b/.agents/skills/hugo-talks-sync/SKILL.md new file mode 100644 index 0000000000..e62925f2f0 --- /dev/null +++ b/.agents/skills/hugo-talks-sync/SKILL.md @@ -0,0 +1,137 @@ +--- +name: hugo-talks-sync +description: Discover missing Recent & Upcoming Talks for this Hugo/HugoBlox repository by comparing shunk031's external talk sources with existing content/event entries. Use when asked to update, audit, backfill, or synchronize talks from Speaker Deck profiles, generic RSS/Atom feeds, slide lists, organizer pages, or other external talk indexes before creating event pages with hugo-event-intake. +--- + +# Hugo Talks Sync + +Find candidate talks that are present on external indexes but missing from +`content/event`, then route accepted candidates through `hugo-event-intake`. + +Use this skill as the discovery layer. Use `hugo-event-intake` as the write layer. +Do not create or overwrite event pages directly from this skill. + +## Workflow + +1. Audit external sources. + +Run the helper from the repository root: + +```bash +uv run python .agents/skills/hugo-talks-sync/scripts/hugo_talks_sync.py audit --repo-root . --output /tmp/hugo-talks-sync.json +``` + +Defaults: + +- Source adapter: `speakerdeck-profile` +- Speaker Deck profile URL: `https://speakerdeck.com/shunk031` +- Existing event root: `content/event` +- Duplicate check fields: `event_url`, `url_slides`, `url_pdf`, `url_video`, `url_code`, and Markdown body URLs + +Use source options when the user names different sources: + +- `--profile-url ` changes the default Speaker Deck profile. +- `--feed-url ` adds a generic auto-detected RSS/Atom `feed` source. Repeat it for multiple feeds. +- `--source-config ` loads explicit source definitions. + +Source config shape: + +```json +{ + "sources": [ + { + "name": "speakerdeck-shunk031", + "adapter": "speakerdeck-profile", + "profile_url": "https://speakerdeck.com/shunk031", + "options": {} + }, + { + "name": "personal-talk-feed", + "adapter": "atom", + "url": "https://example.com/talks.atom", + "options": {} + } + ] +} +``` + +First-class adapters are currently `speakerdeck-profile`, auto-detected `feed`, +explicit `rss`, and explicit `atom`. For HTML index pages, APIs, or +platform-specific sources such as connpass users or YouTube channels, add a +source adapter to the registry in +`scripts/hugo_talks_sync.py` instead of scraping ad hoc in the workflow. + +2. Review `/tmp/hugo-talks-sync.json`. + +For each candidate, inspect: + +- `source_url`: the Speaker Deck page or external source +- `source_name`, `adapter`, and `source_type` (a compatibility alias of `adapter`) +- `title` +- `published_at` +- `image_url` +- `description` +- `extracted_urls`: organizer, paper, code, video, or PDF URLs found in the feed description +- `likely_kind` and `suggested_tags`: tentative classification to carry into + `hugo-event-intake` review. Empty `likely_kind` means the source did not + provide enough evidence; resolve it before writing. +- `suggested_intake_inputs`: URLs to pass to `hugo-event-intake` + +Skip candidates marked `existing_path`. They already match an event entry. + +3. Cross-check uncertain candidates. + +Read [references/source-policy.md](references/source-policy.md) when deciding which +external facts are sufficient. In short: feeds are enough to identify source +items, but not always enough to determine event name, exact talk time, location, +or talk kind. Prefer organizer pages discovered in `extracted_urls`. + +4. Present a proposal before writing. + +Show the user a compact table of missing candidates with: + +- title +- source URL +- date from the feed +- likely organizer URL if discovered +- any missing fields that `hugo-event-intake` will need to resolve + +Ask which candidates to create. Do not assume that every Speaker Deck upload should +be a site event; some decks are notes, paper readings, or drafts. +Do not keep `Speaker Deck` as the event name or `Report` as the kind when the +description or discovered URLs show that the deck is a paper reading or journal +club entry. + +5. Create accepted events through `hugo-event-intake`. + +For each accepted candidate, run the existing intake probe with the suggested URLs: + +```bash +uv run python .agents/skills/hugo-event-intake/scripts/hugo_event_intake.py probe --repo-root . --input --input --output /tmp/event-spec.json +``` + +Then follow the `hugo-event-intake` confirm-then-write workflow. Keep one +`/tmp/event-spec-.json` per candidate when processing several talks. + +## Source Matching Rules + +- Treat exact normalized URLs as duplicates. +- Also inspect Markdown body URLs because older event pages may include source links + outside top-level frontmatter. +- Keep slide-deck sources such as Speaker Deck as `url_slides`. +- Pass organizer URLs such as connpass or official conference pages as additional + `hugo-event-intake` inputs. +- Use RSS/Atom and source-specific public metadata first. Avoid scraping HTML + unless a source adapter owns that parsing. +- Keep adapter-specific settings in each source's `options` object so future + HTML/API adapters can own selectors, pagination, API parameters, or URL-role + rules without changing the audit workflow. + +## When To Stop + +- Stop if Speaker Deck or the named feed is unavailable and the user did not provide + another source list. +- Stop before writing if official organizer metadata conflicts with Speaker Deck + title, date, or description. +- Stop before creating pages when the candidate is not clearly a public talk, + presentation, poster, report, or journal club entry. diff --git a/.agents/skills/hugo-talks-sync/agents/openai.yaml b/.agents/skills/hugo-talks-sync/agents/openai.yaml new file mode 100644 index 0000000000..08a9f82815 --- /dev/null +++ b/.agents/skills/hugo-talks-sync/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Hugo Talks Sync" + short_description: "Find missing talks and feed Hugo event intake." + default_prompt: "Use $hugo-talks-sync to compare shunk031's Speaker Deck profile with content/event, propose missing talks, and hand accepted URLs to $hugo-event-intake." diff --git a/.agents/skills/hugo-talks-sync/references/source-policy.md b/.agents/skills/hugo-talks-sync/references/source-policy.md new file mode 100644 index 0000000000..b49c4c941e --- /dev/null +++ b/.agents/skills/hugo-talks-sync/references/source-policy.md @@ -0,0 +1,29 @@ +# Source Policy + +Use this reference when `hugo-talks-sync` finds candidate talks. + +## Authority Order + +1. Existing `content/event` entries decide whether a source is already registered. +2. Official organizer pages decide event name, exact date/time, location, and session context. +3. Source feeds and source-specific metadata decide item title, source URL, cover image, and description. +4. Secondary sources may confirm facts, but should not be the only source for event metadata. + +## Source Handling + +- Prefer explicit source adapters over one-off scraping. +- Use `speakerdeck-profile` for Speaker Deck profiles, `feed` for auto-detected RSS/Atom sources, and explicit `rss` or `atom` adapters when the expected feed format is known. +- Treat feed `pubDate`, `published`, or `updated` as source publication dates until an organizer page confirms the actual event time. +- Treat URLs inside descriptions or summaries as discovery hints. Inspect them before writing. +- Add a new adapter when a source needs platform-specific semantics, such as user profile pagination, API authentication, HTML selectors, video metadata, or source-specific URL roles. Put adapter-specific settings in `options`. + +## Candidate Classification + +- Paper-reading titles or descriptions usually become `Journal Club`. +- Speaker Deck-only items that cite a paper URL such as arXiv or describe a + survey/paper explanation usually become `Journal Club`, not `Report`. +- Invited event pages, seminars, and conference talks usually become `Invited Talk`. +- Conference recap decks usually become `Report`. +- Poster sessions or explicitly poster-like pages usually become `Invited Poster`. +- If classification is unclear, leave it for `hugo-event-intake` confirmation instead of guessing. + An empty `likely_kind` in audit output is preferable to a false `Invited Talk`. diff --git a/.agents/skills/hugo-talks-sync/scripts/hugo_talks_sync.py b/.agents/skills/hugo-talks-sync/scripts/hugo_talks_sync.py new file mode 100755 index 0000000000..b0c58428ba --- /dev/null +++ b/.agents/skills/hugo-talks-sync/scripts/hugo_talks_sync.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import re +import sys +import xml.etree.ElementTree as ET +from dataclasses import asdict, dataclass, field +from html import unescape +from pathlib import Path +from typing import Any +from urllib.parse import urljoin, urlsplit, urlunsplit +from urllib.request import Request, urlopen + + +URL_FIELDS = ("event_url", "url_slides", "url_pdf", "url_video", "url_code") +USER_AGENT = "Mozilla/5.0 (compatible; hugo-talks-sync/1.0)" +RSS_NAMESPACES = { + "atom": "http://www.w3.org/2005/Atom", + "content": "http://purl.org/rss/1.0/modules/content/", + "dc": "http://purl.org/dc/elements/1.1/", + "media": "http://search.yahoo.com/mrss/", +} +URL_RE = re.compile(r"https?://[^\s<>()\"']+") +KIND_TAGS = { + "Invited Talk": ["Invited Talk"], + "Journal Club": ["Journal Club", "Paper Reading"], + "Invited Presentation": ["Invited Talk", "Invited Presentation"], + "Invited Poster": ["Presentation"], + "Report": ["Report"], +} +REPORT_KEYWORDS = ( + "report", + "速報", + "参加報告", + "参加報告会", +) +JOURNAL_CLUB_KEYWORDS = ( + "journal club", + "reading", + "paper reading", + "論文読み会", + "読み会", + "輪読", + "論文", + "paper", + "arxiv.org", + "transformer-circuits.pub", + "survey on", + "a survey", + "サーベイ論文", + "提案について紹介", +) + + +@dataclass(frozen=True) +class SourceConfig: + name: str + adapter: str + url: str + options: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ExistingEvent: + path: str + urls: list[str] + + +@dataclass +class FeedEntry: + source_url: str + title: str + source_name: str + adapter: str + published_at: str = "" + description: str = "" + image_url: str = "" + extracted_urls: list[str] = field(default_factory=list) + duplicate_of: str = "" + existing_path: str = "" + + @property + def suggested_intake_inputs(self) -> list[str]: + inputs = [self.source_url] if self.source_url else [] + for url in self.extracted_urls: + if url not in inputs: + inputs.append(url) + return inputs + + def to_dict(self) -> dict[str, Any]: + payload = asdict(self) + payload["source_type"] = self.adapter + payload["likely_kind"] = infer_candidate_kind(self) + payload["suggested_tags"] = suggested_tags_for_kind(payload["likely_kind"]) + payload["suggested_intake_inputs"] = self.suggested_intake_inputs + return payload + + +def suggested_tags_for_kind(kind: str) -> list[str]: + return list(KIND_TAGS.get(kind, [kind] if kind else [])) + + +def infer_candidate_kind(entry: FeedEntry) -> str: + haystack = " ".join( + [ + entry.title, + entry.description, + entry.source_url, + *entry.extracted_urls, + ] + ).lower() + if "poster" in haystack: + return "Invited Poster" + if any( + keyword in haystack + for keyword in ("invited presentation", "doctoral dissertation presentation") + ): + return "Invited Presentation" + if any(keyword in haystack for keyword in REPORT_KEYWORDS): + return "Report" + if any(keyword in haystack for keyword in JOURNAL_CLUB_KEYWORDS): + return "Journal Club" + return "" + + +def normalize_url(url: str) -> str: + parsed = urlsplit(url.strip()) + path = parsed.path.rstrip("/") or parsed.path or "/" + return urlunsplit( + (parsed.scheme.lower(), parsed.netloc.lower(), path, parsed.query, "") + ) + + +def read_text_url(url: str) -> str: + request = Request(url, headers={"User-Agent": USER_AGENT}) + with urlopen(request) as response: + data = response.read() + charset = response.headers.get_content_charset() or "utf-8" + return data.decode(charset, errors="replace") + + +def default_feed_url(profile_url: str) -> str: + return profile_url.rstrip("/") + ".rss" + + +def source_name_from_url(url: str) -> str: + parsed = urlsplit(url) + candidate = f"{parsed.netloc}{parsed.path}".strip("/") + name = re.sub(r"[^a-zA-Z0-9]+", "-", candidate).strip("-").lower() + return name or "feed" + + +def default_sources(profile_url: str) -> list[SourceConfig]: + return [ + SourceConfig( + name=source_name_from_url(profile_url), + adapter="speakerdeck-profile", + url=default_feed_url(profile_url), + options={"profile_url": profile_url}, + ) + ] + + +def normalize_source_config(raw: dict[str, Any], index: int) -> SourceConfig: + adapter = str( + raw.get("adapter") or raw.get("type") or raw.get("source_type") or "" + ).strip() + if adapter not in SOURCE_ADAPTERS: + raise ValueError( + f"Unsupported source adapter at index {index}: {adapter!r}. " + f"Supported adapters are {', '.join(sorted(SOURCE_ADAPTERS))}." + ) + raw_options = raw.get("options") or {} + if not isinstance(raw_options, dict): + raise ValueError(f"source options at index {index} must be an object.") + options = dict(raw_options) + profile_url = str(raw.get("profile_url") or "").strip() + url = str(raw.get("url") or "").strip() + if profile_url: + options["profile_url"] = profile_url + if adapter == "speakerdeck-profile": + if not profile_url and url.endswith((".rss", ".atom")): + profile_url = url.rsplit(".", 1)[0] + options["profile_url"] = profile_url + if not profile_url: + raise ValueError( + f"speakerdeck-profile source at index {index} requires profile_url." + ) + if not url: + url = default_feed_url(profile_url) + elif not url: + raise ValueError(f"{adapter} source at index {index} requires url.") + name = str(raw.get("name") or source_name_from_url(profile_url or url)).strip() + return SourceConfig( + name=name, + adapter=adapter, + url=url, + options=options, + ) + + +def load_source_configs(path: Path) -> list[SourceConfig]: + payload = json.loads(path.read_text(encoding="utf-8")) + if isinstance(payload, list): + raw_sources = payload + elif isinstance(payload, dict): + raw_sources = payload.get("sources", []) + else: + raise ValueError("source config must be a JSON object or array.") + if not isinstance(raw_sources, list) or not raw_sources: + raise ValueError("source config must contain a non-empty 'sources' list.") + return [ + normalize_source_config(raw_source, index) + for index, raw_source in enumerate(raw_sources) + ] + + +def parse_scalar(raw: str) -> str: + text = raw.strip() + if not text or text in {"null", "None"}: + return "" + if text.startswith(("'", '"')) and text.endswith(("'", '"')) and len(text) >= 2: + return text[1:-1] + return text + + +def extract_urls(text: str) -> list[str]: + urls: list[str] = [] + seen: set[str] = set() + for match in URL_RE.finditer(unescape(text or "")): + url = match.group(0).rstrip(".,;]") + normalized = normalize_url(url) + if normalized in seen: + continue + seen.add(normalized) + urls.append(url) + return urls + + +def read_top_level_frontmatter_urls(text: str) -> list[str]: + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return [] + urls: list[str] = [] + for line in lines[1:]: + if line.strip() == "---": + break + if line.startswith(" ") or line.startswith("-") or ":" not in line: + continue + key, value = line.split(":", 1) + if key.strip() not in URL_FIELDS: + continue + url = parse_scalar(value) + if url.startswith(("http://", "https://")): + urls.append(url) + return urls + + +def load_existing_events(repo_root: Path) -> list[ExistingEvent]: + events: list[ExistingEvent] = [] + event_root = repo_root / "content" / "event" + for index_md in sorted(event_root.glob("*/index.md")): + text = index_md.read_text(encoding="utf-8") + urls = read_top_level_frontmatter_urls(text) + urls.extend(extract_urls(text)) + normalized_urls = sorted({normalize_url(url) for url in urls}) + events.append( + ExistingEvent( + path=str(index_md.relative_to(repo_root)), + urls=normalized_urls, + ) + ) + return events + + +def find_existing_path(existing_events: list[ExistingEvent], urls: list[str]) -> str: + normalized = { + normalize_url(url) for url in urls if url.startswith(("http://", "https://")) + } + if not normalized: + return "" + for event in existing_events: + if normalized.intersection(event.urls): + return event.path + return "" + + +def first_text(element: ET.Element, names: tuple[str, ...]) -> str: + for name in names: + found = element.find(name, RSS_NAMESPACES) + if found is not None and found.text: + return unescape(found.text).strip() + return "" + + +def first_attr(element: ET.Element, names: tuple[str, ...], attr: str) -> str: + for name in names: + found = element.find(name, RSS_NAMESPACES) + if found is not None: + value = found.attrib.get(attr, "").strip() + if value: + return value + return "" + + +def assert_root(root: ET.Element, expected: str, source: SourceConfig) -> None: + actual = root.tag.rsplit("}", 1)[-1].lower() + if expected == "rss" and actual != "rss": + raise ValueError(f"{source.name} expected RSS XML, got {root.tag!r}.") + if expected == "atom" and actual != "feed": + raise ValueError(f"{source.name} expected Atom XML, got {root.tag!r}.") + + +def detect_xml_format(root: ET.Element, source: SourceConfig) -> str: + actual = root.tag.rsplit("}", 1)[-1].lower() + if actual == "rss": + return "rss" + if actual == "feed": + return "atom" + raise ValueError( + f"{source.name} uses auto feed detection, but XML root {root.tag!r} " + "is neither RSS nor Atom." + ) + + +def parse_rss_entries(root: ET.Element, source: SourceConfig) -> list[FeedEntry]: + entries: list[FeedEntry] = [] + for item in root.findall("./channel/item"): + description = first_text(item, ("description", "content:encoded")) + source_url = first_text(item, ("link", "guid")) + image_url = first_attr(item, ("media:content",), "url") + extracted = [ + url + for url in extract_urls(description) + if normalize_url(url) != normalize_url(source_url) + ] + entries.append( + FeedEntry( + source_url=source_url, + title=first_text(item, ("title",)), + source_name=source.name, + adapter=source.adapter, + published_at=first_text(item, ("pubDate", "dc:date")), + description=description, + image_url=image_url, + extracted_urls=extracted, + ) + ) + return entries + + +def parse_atom_entries(root: ET.Element, source: SourceConfig) -> list[FeedEntry]: + entries: list[FeedEntry] = [] + for entry in root.findall("atom:entry", RSS_NAMESPACES): + source_url = "" + for link in entry.findall("atom:link", RSS_NAMESPACES): + rel = link.attrib.get("rel", "alternate") + href = link.attrib.get("href", "") + if href and rel == "alternate": + source_url = urljoin(source.url, href) + break + description = first_text(entry, ("atom:summary", "atom:content")) + extracted = [ + url + for url in extract_urls(description) + if normalize_url(url) != normalize_url(source_url) + ] + entries.append( + FeedEntry( + source_url=source_url, + title=first_text(entry, ("atom:title",)), + source_name=source.name, + adapter=source.adapter, + published_at=first_text(entry, ("atom:published", "atom:updated")), + description=description, + image_url="", + extracted_urls=extracted, + ) + ) + return entries + + +def parse_feed_entries(feed_xml: str, source: SourceConfig) -> list[FeedEntry]: + root = ET.fromstring(feed_xml) + xml_format = source.adapter + if xml_format == "speakerdeck-profile": + xml_format = "rss" + elif xml_format == "feed": + xml_format = detect_xml_format(root, source) + assert_root(root, xml_format, source) + if xml_format == "rss": + return parse_rss_entries(root, source) + if xml_format == "atom": + return parse_atom_entries(root, source) + raise ValueError(f"{source.name} cannot parse XML as adapter {source.adapter!r}.") + + +class FeedSourceAdapter: + def __init__(self, source: SourceConfig) -> None: + self.source = source + + def entries(self) -> list[FeedEntry]: + return parse_feed_entries(read_text_url(self.source.url), self.source) + + +SOURCE_ADAPTERS = { + "speakerdeck-profile": FeedSourceAdapter, + "feed": FeedSourceAdapter, + "rss": FeedSourceAdapter, + "atom": FeedSourceAdapter, +} + + +def build_source_adapter(source: SourceConfig) -> FeedSourceAdapter: + adapter_type = SOURCE_ADAPTERS.get(source.adapter) + if not adapter_type: + raise ValueError(f"Unsupported source adapter: {source.adapter}") + return adapter_type(source) + + +def audit(repo_root: Path, sources: list[SourceConfig]) -> dict[str, Any]: + existing_events = load_existing_events(repo_root) + candidates: list[FeedEntry] = [] + existing: list[FeedEntry] = [] + duplicates: list[FeedEntry] = [] + seen_candidate_urls: dict[str, str] = {} + for source in sources: + for entry in build_source_adapter(source).entries(): + urls = [entry.source_url, *entry.extracted_urls] + entry.existing_path = find_existing_path(existing_events, urls) + if entry.existing_path: + existing.append(entry) + elif entry.source_url and normalize_url(entry.source_url) in seen_candidate_urls: + entry.duplicate_of = seen_candidate_urls[normalize_url(entry.source_url)] + duplicates.append(entry) + else: + if entry.source_url: + seen_candidate_urls[normalize_url(entry.source_url)] = entry.source_url + candidates.append(entry) + return { + "candidate_count": len(candidates), + "existing_count": len(existing), + "duplicate_count": len(duplicates), + "candidates": [entry.to_dict() for entry in candidates], + "existing": [entry.to_dict() for entry in existing], + "duplicates": [entry.to_dict() for entry in duplicates], + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Find external talk feed entries missing from content/event." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + audit_parser = subparsers.add_parser("audit") + audit_parser.add_argument("--repo-root", type=Path, default=Path(".")) + audit_parser.add_argument( + "--profile-url", default="https://speakerdeck.com/shunk031" + ) + audit_parser.add_argument("--feed-url", action="append", default=[]) + audit_parser.add_argument( + "--source-config", + type=Path, + help=( + "JSON file with sources. Supports adapters 'speakerdeck-profile', " + "'feed', 'rss', and 'atom'." + ), + ) + audit_parser.add_argument("--output", type=Path) + audit_parser.add_argument("--include-existing", action="store_true") + args = parser.parse_args() + + if args.command == "audit": + repo_root = args.repo_root.resolve() + if args.source_config: + sources = load_source_configs(args.source_config) + elif args.feed_url: + sources = [ + SourceConfig( + name=source_name_from_url(feed_url), + adapter="feed", + url=feed_url, + ) + for feed_url in args.feed_url + ] + else: + sources = default_sources(args.profile_url) + result = audit(repo_root, sources) + if not args.include_existing: + result.pop("existing", None) + payload = json.dumps(result, ensure_ascii=False, indent=2) + if args.output: + args.output.write_text(payload + "\n", encoding="utf-8") + else: + print(payload) + return 0 + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.lychee/exclude-temporary.txt b/.lychee/exclude-temporary.txt index a7a7578a79..13e236f736 100644 --- a/.lychee/exclude-temporary.txt +++ b/.lychee/exclude-temporary.txt @@ -12,6 +12,9 @@ drive.google.com/file/d/0B3O7bgd3mym6N214SWw3eVJCS3M/view t.co openai.com/index/chatgpt www.ieice.org/~nlc +# IEICE KEN paper page returns HTTP 403 to automated checks even with a browser +# user-agent. Re-check regularly in case the access policy changes. +https://ken.ieice.org/ken/paper/20190927q1Qe/ www.nikkan.co.jp/articles www.cikm2022.org www.kdd.org/kdd2019 diff --git a/AGENTS.md b/AGENTS.md index 5765f12c6d..2453ab4630 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,8 @@ shunk031.me (Hugo 製個人サイト) のエージェント向け作業ガイド ## スキル (定型作業) - 置き場所: `.agents/skills/` が source of truth です。`.claude/skills` はここへの symlink で、Claude Code からも同じスキルが見えます。ツール固有のスキルを重複して作らないでください。 -- 対応表: event ページの追加は `hugo-event-intake`、publication のサムネイル生成は `hugo-publication-thumbnail`、ポスターの Projects 登録は `hugo-poster-project-sync`、採択・発表 news の同期は `hugo-conference-news-sync` を使ってください。呼び出しコマンドは各 `SKILL.md` を参照し、conference news 同期は top-level の `scripts/sync_conference_news.py` を使います (スキル配下に同名スクリプトがあっても top-level 版が正です)。 +- 対応表: Recent & Upcoming Talks の更新・外部ソース差分確認・talks 同期は `hugo-talks-sync` skill を先に使い、候補の event ページ作成は `hugo-event-intake` skill に渡してください。 +- 単発の event ページ追加は `hugo-event-intake` skill、publication のサムネイル生成は `hugo-publication-thumbnail` skill、ポスターの Projects 登録は `hugo-poster-project-sync` skill、採択・発表 news の同期は `hugo-conference-news-sync` skill を使ってください。呼び出しコマンドは各 skill の `SKILL.md` を参照し、conference news 同期は top-level の `scripts/sync_conference_news.py` を使います (スキル配下に同名スクリプトがあっても top-level 版が正です)。 ## リンク切れの運用 diff --git a/content/event/can-we-reliably-control-llms/featured.jpg b/content/event/can-we-reliably-control-llms/featured.jpg new file mode 100644 index 0000000000..4094b4b0e3 Binary files /dev/null and b/content/event/can-we-reliably-control-llms/featured.jpg differ diff --git a/content/event/can-we-reliably-control-llms/index.md b/content/event/can-we-reliably-control-llms/index.md new file mode 100644 index 0000000000..51c53c6737 --- /dev/null +++ b/content/event/can-we-reliably-control-llms/index.md @@ -0,0 +1,53 @@ +--- +title: "その LLM 制御、本当に信頼できますか? / Can We Reliably Control LLMs?" +event: "【Sansan × IVRy】NLP2026 参加報告会" +event_url: "https://sansan.connpass.com/event/388590/" +location: "Sansan株式会社 本社" +address: + street: "桜丘町1-1 渋谷サクラステージ 28F" + city: "Shibuya-ku" + region: "Tokyo" + postcode: "150-6228" + country: "Japan" +summary: "NLP2026 参加報告会で、大規模言語モデルの応答を調整する技術の信頼性と限界を発表" +abstract: "大規模言語モデル(LLM)に対する内部介入(steering / intervention)は、重み更新を伴わずに推論時の振る舞いを制御できる手法として、近年急速に注目を集めています。本発表では、この軽量かつ柔軟な制御パラダイムの可能性と限界について、言語処理学会 2026 における関連研究を体系的に整理しながら概観します。特に、北田+「ステアリングベクトルは日本語LLMを堅牢に制御できるか?」を中心に、評価・安全性・多言語制御・表現学習といった観点から、内部介入がどこまで信頼できる制御として機能するのかを議論します。\n\n■【Sansan × IVRy】NLP2026 参加報告会\nhttps://sansan.connpass.com/event/388590/ \n\n■ 登壇概要\nタイトル:その LLM 制御、本当に信頼できますか?\n\n■ 言語処理学会第32回年次大会(NLP2026)\nhttps://www.anlp.jp/nlp2026/ \n\n■ 北田+ \"ステアリングベクトルは日本語 LLM を堅牢に制御できるか?\"\nhttps://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B1-13.pdf" +date: "2026-04-21T19:45:00+09:00" +date_end: "2026-04-21T19:55:00+09:00" +all_day: false +publishDate: "2026-04-21T19:45:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Report", "NLP"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: +- name: "Connpass" + url: "https://sansan.connpass.com/event/388590/" +url_slides: "https://speakerdeck.com/shunk031/can-we-reliably-control-llms" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# 【Sansan × IVRy】NLP2026 参加報告会 + +> 大規模言語モデル(LLM)に対する内部介入(steering / intervention)は、重み更新を伴わずに推論時の振る舞いを制御できる手法として、近年急速に注目を集めています。本発表では、この軽量かつ柔軟な制御パラダイムの可能性と限界について、言語処理学会 2026 における関連研究を体系的に整理しながら概観します。特に、北田+「ステアリングベクトルは日本語LLMを堅牢に制御できるか?」を中心に、評価・安全性・多言語制御・表現学習といった観点から、内部介入がどこまで信頼できる制御として機能するのかを議論します。 +> +> ■【Sansan × IVRy】NLP2026 参加報告会 +> https://sansan.connpass.com/event/388590/ +> +> ■ 登壇概要 +> タイトル:その LLM 制御、本当に信頼できますか? +> +> ■ 言語処理学会第32回年次大会(NLP2026) +> https://www.anlp.jp/nlp2026/ +> +> ■ 北田+ "ステアリングベクトルは日本語 LLM を堅牢に制御できるか?" +> https://www.anlp.jp/proceedings/annual_meeting/2026/pdf_dir/B1-13.pdf + +## 資料 - Slides + + diff --git a/content/event/ccse-2019/index.md b/content/event/ccse-2019/index.md index 8b34e22a42..f40d66f550 100644 --- a/content/event/ccse-2019/index.md +++ b/content/event/ccse-2019/index.md @@ -25,7 +25,7 @@ all_day: false publishDate: 2019-07-13T00:00:00+09:00 authors: ["Shunsuke Kitada", "Mitsumasa Kubo"] -tags: ["Invited talk"] +tags: ["Invited Talk"] # Is this a featured talk? (true/false) featured: false diff --git a/content/event/eccv2024-report/index.md b/content/event/eccv2024-report/index.md index a76e9031e8..b0fb8f66ad 100644 --- a/content/event/eccv2024-report/index.md +++ b/content/event/eccv2024-report/index.md @@ -12,7 +12,7 @@ address: region: postcode: country: -summary: ECCV(European Conference on Computer Vision)はコンピュータビジョン分野トップ国際会議のひとつです。今回は分野の動向・気付きをまとめることに注力し、132ページの資料として仕上げることができました。 +summary: ECCV 2024 速報に、creative graphic design 関連論文のまとめを寄稿 abstract: # Talk start and end times. diff --git a/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/featured.jpg b/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/featured.jpg new file mode 100644 index 0000000000..a9a9dc057a Binary files /dev/null and b/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/featured.jpg differ diff --git a/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/index.md b/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/index.md new file mode 100644 index 0000000000..a3d073ca27 --- /dev/null +++ b/content/event/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design/index.md @@ -0,0 +1,43 @@ +--- +title: "[Journal Club] AIグラフィックデザインの進化:断片から統合(One Piece)へ / From Fragment to One Piece: A Survey on AI-Driven Graphic Design" +event: "Journal Club" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "AI を使ったグラフィックデザイン研究の生成・編集・評価の動向を紹介" +abstract: "AI技術はグラフィックデザインの領域に革命をもたらし、クリエイティブなプロセスを根本から変えようとしています。かつてはタイポグラフィやレイアウトなど個々の要素を扱う断片的な研究が主流でしたが、近年の大規模言語モデル(LLM)やマルチモーダル技術の進化は、デザイン全体を統合的に扱う新たな潮流を生み出しています。\n\n本発表では、この急速に進化するAI駆動グラフィックデザイン(AIGD)の全貌を、最新の包括的サーベイ論文「From Fragment to One Piece: A Survey on AI-Driven Graphic Design (Zou et al. - 2025)」 に基づき詳細に解説します。\n\n- 📝:https://arxiv.org/abs/2507.17202" +date: "2025-08-31T12:00:00+09:00" +date_end: "2025-08-31T12:00:00+09:00" +all_day: false +publishDate: "2025-08-31T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "Graphic Design", "Survey"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/from-fragment-to-one-piece-a-survey-on-ai-driven-graphic-design" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Journal Club + +> AI技術はグラフィックデザインの領域に革命をもたらし、クリエイティブなプロセスを根本から変えようとしています。かつてはタイポグラフィやレイアウトなど個々の要素を扱う断片的な研究が主流でしたが、近年の大規模言語モデル(LLM)やマルチモーダル技術の進化は、デザイン全体を統合的に扱う新たな潮流を生み出しています。 +> +> 本発表では、この急速に進化するAI駆動グラフィックデザイン(AIGD)の全貌を、最新の包括的サーベイ論文「From Fragment to One Piece: A Survey on AI-Driven Graphic Design (Zou et al. - 2025)」 に基づき詳細に解説します。 +> +> - 📝:https://arxiv.org/abs/2507.17202 + +## 資料 - Slides + + diff --git a/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/featured.jpg b/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/featured.jpg new file mode 100644 index 0000000000..b4a7825c19 Binary files /dev/null and b/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/featured.jpg differ diff --git a/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/index.md b/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/index.md new file mode 100644 index 0000000000..dfbf18139c --- /dev/null +++ b/content/event/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words/index.md @@ -0,0 +1,47 @@ +--- +title: "[Journal Club] Human Attention Maps for Text Classification: Do Humans and Neural Networks Focus on the Same Words?" +event: "ACL 2020 オンラインLT会" +event_url: "https://nlpaper-challenge.connpass.com/event/185240/" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "ACL 2020 オンラインLT会で、人間とモデルの注目箇所を比較する研究を紹介" +abstract: "人手によってアノテーションしたattention mapを元に、深層学習モデルのattention mapと比較分析を行った。\n\n■ イベント:ACL 2020 オンラインLT会 \nhttps://nlpaper-challenge.connpass.com/event/185240/\n\n■ 登壇概要\nタイトル:Human Attention Maps for Text Classification: Do Humans and Neural Networks Focus on the Same Words?" +date: "2020-08-17T20:10:00+09:00" +date_end: "2020-08-17T20:30:00+09:00" +all_day: false +publishDate: "2020-08-17T20:10:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "ACL"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: +- name: "Connpass" + url: "https://nlpaper-challenge.connpass.com/event/185240/" +url_slides: "https://speakerdeck.com/shunk031/human-attention-maps-for-text-classification-do-humans-and-neural-networks-focus-on-the-same-words" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# ACL 2020 オンラインLT会 + +> 人手によってアノテーションしたattention mapを元に、深層学習モデルのattention mapと比較分析を行った。 +> +> ■ イベント:ACL 2020 オンラインLT会 +> https://nlpaper-challenge.connpass.com/event/185240/ +> +> ■ 登壇概要 +> タイトル:Human Attention Maps for Text Classification: Do Humans and Neural Networks Focus on the Same Words? + +## 資料 - Slides + + diff --git a/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/featured.jpg b/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/featured.jpg new file mode 100644 index 0000000000..9f7c2a16ba Binary files /dev/null and b/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/featured.jpg differ diff --git a/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/index.md b/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/index.md new file mode 100644 index 0000000000..0da1b2cd30 --- /dev/null +++ b/content/event/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation/index.md @@ -0,0 +1,44 @@ +--- +title: "[Invited Presentation] Improving Prediction Performance and Model Interpretability through Attention Mechanisms from Basic and Applied Research Perspectives" +event: "Doctoral Dissertation Presentation" +event_url: "" +location: "Hosei University" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "博士論文発表で、モデルがどこを重視するかを使った予測と解釈の研究を発表" +abstract: "Slides for the presentation of my doctoral dissertation (in Japanese).\nThe contents of the presentation are already published as the following papers:\n- Section 2: Kitada and Iyatomi IEEE Access'21. https://arxiv.org/abs/2009.12064\n- Section 3: Kitada and Iyatomi. Appl. Intell.'22. https://arxiv.org/abs/2104.08763\n- Section 4: Kitada et al. KDD'19. https://arxiv.org/abs/1905.07289\n- Section 5: Kitada et al. Appl. Sci.'22. https://arxiv.org/abs/2204.11588" +date: "2023-03-24T12:00:00+09:00" +date_end: "2023-03-24T12:00:00+09:00" +all_day: false +publishDate: "2023-03-24T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Invited Talk", "Invited Presentation"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/improving-prediction-performance-and-model-interpretability-through-attention-mechanisms-from-basic-and-applied-research-perspectives-presentation-for-doctoral-dissertation" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Doctoral Dissertation Presentation + +> Slides for the presentation of my doctoral dissertation (in Japanese). +> The contents of the presentation are already published as the following papers: +> - Section 2: Kitada and Iyatomi IEEE Access'21. https://arxiv.org/abs/2009.12064 +> - Section 3: Kitada and Iyatomi. Appl. Intell.'22. https://arxiv.org/abs/2104.08763 +> - Section 4: Kitada et al. KDD'19. https://arxiv.org/abs/1905.07289 +> - Section 5: Kitada et al. Appl. Sci.'22. https://arxiv.org/abs/2204.11588 + +## 資料 - Slides + + diff --git a/content/event/kantocv-67th-cvpr-2026/featured.jpg b/content/event/kantocv-67th-cvpr-2026/featured.jpg new file mode 100644 index 0000000000..1cc870e2d4 Binary files /dev/null and b/content/event/kantocv-67th-cvpr-2026/featured.jpg differ diff --git a/content/event/kantocv-67th-cvpr-2026/index.md b/content/event/kantocv-67th-cvpr-2026/index.md new file mode 100644 index 0000000000..bc34058d10 --- /dev/null +++ b/content/event/kantocv-67th-cvpr-2026/index.md @@ -0,0 +1,47 @@ +--- +title: "[Journal Club] PSDesigner: Automated Graphic Design with a Human-Like Creative Workflow (CVPR 2026)" +event: "第67回 コンピュータビジョン勉強会@関東(前編)" +event_url: "https://kantocv.connpass.com/event/397919/" +location: "DeNA / IRIAM" +address: + street: "渋谷二丁目24番12号 渋谷スクランブルスクエア 40F" + city: "Shibuya-ku" + region: "Tokyo" + postcode: "150-6140" + country: "Japan" +summary: "CVPR 2026 論文読み会で、生成後も要素を編集できるデザイン生成手法を紹介" +abstract: "デザイン素材収集と、Photoshopの操作履歴から学習した Vision and Language Model (VLM) によるツール呼び出しの計画・実行を組み合わせ、Agentic に複雑なレイヤー構造をもつ編集可能なデザインを生成する PSDesigner について紹介します。\n\n■ 第67回 コンピュータビジョン勉強会@関東(前編)\nhttps://kantocv.connpass.com/event/397919/\n\n■ 登壇概要\nタイトル:PSDesigner: Automated Graphic Design with a Human-Like Creative Workflow" +date: "2026-07-18T13:45:00+09:00" +date_end: "2026-07-18T14:10:00+09:00" +all_day: false +publishDate: "2026-07-18T13:45:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "CVPR"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: +- name: "Connpass" + url: "https://kantocv.connpass.com/event/397919/" +url_slides: "https://speakerdeck.com/shunk031/kantocv-67th-cvpr-2026" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# 第67回 コンピュータビジョン勉強会@関東(前編) + +> デザイン素材収集と、Photoshopの操作履歴から学習した Vision and Language Model (VLM) によるツール呼び出しの計画・実行を組み合わせ、Agentic に複雑なレイヤー構造をもつ編集可能なデザインを生成する PSDesigner について紹介します。 +> +> ■ 第67回 コンピュータビジョン勉強会@関東(前編) +> https://kantocv.connpass.com/event/397919/ +> +> ■ 登壇概要 +> タイトル:PSDesigner: Automated Graphic Design with a Human-Like Creative Workflow + +## 資料 - Slides + + diff --git a/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/featured.jpg b/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/featured.jpg new file mode 100644 index 0000000000..97bbfc0255 Binary files /dev/null and b/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/featured.jpg differ diff --git a/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/index.md b/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/index.md new file mode 100644 index 0000000000..c261337ccb --- /dev/null +++ b/content/event/large-language-model-agent-a-survey-on-methodology-applications-and-challenges/index.md @@ -0,0 +1,42 @@ +--- +title: "[Journal Club] Large Language Model Agent: A Survey on Methodology, Applications and Challenges" +event: "Journal Club" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "大規模言語モデルが道具を使って課題を進める仕組みと課題を紹介" +abstract: "大規模言語モデル(LLM)を搭載した「LLMエージェント」は、自律的な目標達成や動的な環境適応能力により、人工知能の新たなフロンティアを切り開いています 。本資料では、この急速に進化するLLMエージェントの全貌を、最新の包括的サーベイ論文「Large Language Model Agent: A Survey on Methodology, Applications and Challenges (Luo et al. - 2025)」 に基づき詳細に解説します。\n\n- 📝:https://arxiv.org/abs/2503.21460\n- 🐙:https://github.com/luo-junyu/Awesome-Agent-Papers" +date: "2025-05-28T12:00:00+09:00" +date_end: "2025-05-28T12:00:00+09:00" +all_day: false +publishDate: "2025-05-28T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "LLM", "Agent"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/large-language-model-agent-a-survey-on-methodology-applications-and-challenges" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Journal Club + +> 大規模言語モデル(LLM)を搭載した「LLMエージェント」は、自律的な目標達成や動的な環境適応能力により、人工知能の新たなフロンティアを切り開いています 。本資料では、この急速に進化するLLMエージェントの全貌を、最新の包括的サーベイ論文「Large Language Model Agent: A Survey on Methodology, Applications and Challenges (Luo et al. - 2025)」 に基づき詳細に解説します。 +> +> - 📝:https://arxiv.org/abs/2503.21460 +> - 🐙:https://github.com/luo-junyu/Awesome-Agent-Papers + +## 資料 - Slides + + diff --git a/content/event/learning-to-faithfully-rationalize-by-construction/featured.jpg b/content/event/learning-to-faithfully-rationalize-by-construction/featured.jpg new file mode 100644 index 0000000000..94f7f1e1d8 Binary files /dev/null and b/content/event/learning-to-faithfully-rationalize-by-construction/featured.jpg differ diff --git a/content/event/learning-to-faithfully-rationalize-by-construction/index.md b/content/event/learning-to-faithfully-rationalize-by-construction/index.md new file mode 100644 index 0000000000..fa67d9184d --- /dev/null +++ b/content/event/learning-to-faithfully-rationalize-by-construction/index.md @@ -0,0 +1,47 @@ +--- +title: "[Journal Club] Learning to Faithfully Rationalize by Construction" +event: "ACL2020オンライン読み会" +event_url: "https://exawizards.connpass.com/event/184582/" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "ACL2020 オンライン読み会で、モデルが予測根拠を忠実に示す手法を紹介" +abstract: "“Attention is not Explanation” と “Attention is not not Explanation” の著者らによる夢のコラボレーション。本文から根拠を抽出してその根拠を元にタスクを解く新たな枠組みの提案。\n\n■ イベント:ACL2020オンライン読み会\nhttps://exawizards.connpass.com/event/184582/\n\n■ 登壇概要\nタイトル:Learning to Faithfully Rationalize by Construction" +date: "2020-09-06T14:30:00+09:00" +date_end: "2020-09-06T14:50:00+09:00" +all_day: false +publishDate: "2020-09-06T14:30:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "ACL"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: +- name: "Connpass" + url: "https://exawizards.connpass.com/event/184582/" +url_slides: "https://speakerdeck.com/shunk031/learning-to-faithfully-rationalize-by-construction" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# ACL2020オンライン読み会 + +> “Attention is not Explanation” と “Attention is not not Explanation” の著者らによる夢のコラボレーション。本文から根拠を抽出してその根拠を元にタスクを解く新たな枠組みの提案。 +> +> ■ イベント:ACL2020オンライン読み会 +> https://exawizards.connpass.com/event/184582/ +> +> ■ 登壇概要 +> タイトル:Learning to Faithfully Rationalize by Construction + +## 資料 - Slides + + diff --git a/content/event/llm-nonei-bu-wocao-rubekutoru/featured.jpg b/content/event/llm-nonei-bu-wocao-rubekutoru/featured.jpg new file mode 100644 index 0000000000..7496adc041 Binary files /dev/null and b/content/event/llm-nonei-bu-wocao-rubekutoru/featured.jpg differ diff --git a/content/event/llm-nonei-bu-wocao-rubekutoru/index.md b/content/event/llm-nonei-bu-wocao-rubekutoru/index.md new file mode 100644 index 0000000000..cca0a87d70 --- /dev/null +++ b/content/event/llm-nonei-bu-wocao-rubekutoru/index.md @@ -0,0 +1,39 @@ +--- +title: "[Journal Club] LLM の内部を操るベクトル?" +event: "Journal Club" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "大規模言語モデル内部の情報を調整して、応答の傾向を制御する手法を紹介" +abstract: "A Speaker Deck presentation about steering vectors for LLM internal representations." +date: "2026-02-17T12:00:00+09:00" +date_end: "2026-02-17T12:00:00+09:00" +all_day: false +publishDate: "2026-02-17T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "LLM"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/llm-nonei-bu-wocao-rubekutoru" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Journal Club + +> A Speaker Deck presentation about steering vectors for LLM internal representations. + +## 資料 - Slides + + diff --git a/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/featured.jpg b/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/featured.jpg new file mode 100644 index 0000000000..e124604149 Binary files /dev/null and b/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/featured.jpg differ diff --git a/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/index.md b/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/index.md new file mode 100644 index 0000000000..3709d4aa43 --- /dev/null +++ b/content/event/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations/index.md @@ -0,0 +1,43 @@ +--- +title: "[Journal Club] Natural Language Autoencoders Produce Unsupervised Explanations of LLM Activations" +event: "Speaker Deck" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "大規模言語モデルが入力に反応する理由を自然言語で説明する手法を紹介" +abstract: "大規模言語モデル(LLM)の内部状態は、高次元の活性化ベクトルとして表現されており、モデルの推論・判断・潜在的な認識に関する豊かな情報を含んでいます。本資料では、この不透明な内部表現を自然言語で読み解く新たな解釈可能性手法 Natural Language Autoencoders(NLA)について、Anthropicの研究論文「Natural Language Autoencoders Produce Unsupervised Explanations of LLM Activations(Fraser-Taliente, Kantamneni, Ong et al. - 2026)」に基づき、手法・評価・監査応用・限界を体系的に解説します。\n\n- 📝:https://transformer-circuits.pub/2026/nla/\n- 🐙:https://github.com/kitft/natural_language_autoencoders\n- 🌐:https://www.neuronpedia.org/nla" +date: "2026-06-05T12:00:00+09:00" +date_end: "2026-06-05T12:00:00+09:00" +all_day: false +publishDate: "2026-06-05T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "Interpretability"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/natural-language-autoencoders-produce-unsupervised-explanations-of-llm-activations" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Speaker Deck + +> 大規模言語モデル(LLM)の内部状態は、高次元の活性化ベクトルとして表現されており、モデルの推論・判断・潜在的な認識に関する豊かな情報を含んでいます。本資料では、この不透明な内部表現を自然言語で読み解く新たな解釈可能性手法 Natural Language Autoencoders(NLA)について、Anthropicの研究論文「Natural Language Autoencoders Produce Unsupervised Explanations of LLM Activations(Fraser-Taliente, Kantamneni, Ong et al. - 2026)」に基づき、手法・評価・監査応用・限界を体系的に解説します。 +> +> - 📝:https://transformer-circuits.pub/2026/nla/ +> - 🐙:https://github.com/kitft/natural_language_autoencoders +> - 🌐:https://www.neuronpedia.org/nla + +## 資料 - Slides + + diff --git a/content/event/stable-diffusion-3/featured.jpg b/content/event/stable-diffusion-3/featured.jpg new file mode 100644 index 0000000000..a5f15d2c1a Binary files /dev/null and b/content/event/stable-diffusion-3/featured.jpg differ diff --git a/content/event/stable-diffusion-3/index.md b/content/event/stable-diffusion-3/index.md new file mode 100644 index 0000000000..21bef721fb --- /dev/null +++ b/content/event/stable-diffusion-3/index.md @@ -0,0 +1,41 @@ +--- +title: "[Journal Club] Scaling Rectified Flow Transformers for High-Resolution Image Synthesis / Stable Diffusion 3" +event: "Journal Club" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "Stable Diffusion 3 が文章から画像を生成する仕組みを紹介" +abstract: "Stability.AI からこれまでの拡散モデルとは少し異なるパラダイムの新たな Text-to-Image モデル Stable Diffusion 3 (SD3) の提案について紹介します。\n\n一部 GIF アニメーションを用いた図があるため、オリジナルの Google Slide を参照していただくのをおすすめします: https://bit.ly/stable-diffusion-3-explained" +date: "2024-03-27T12:00:00+09:00" +date_end: "2024-03-27T12:00:00+09:00" +all_day: false +publishDate: "2024-03-27T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "Image Generation", "Diffusion Model"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/stable-diffusion-3" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Journal Club + +> Stability.AI からこれまでの拡散モデルとは少し異なるパラダイムの新たな Text-to-Image モデル Stable Diffusion 3 (SD3) の提案について紹介します。 +> +> 一部 GIF アニメーションを用いた図があるため、オリジナルの Google Slide を参照していただくのをおすすめします: https://bit.ly/stable-diffusion-3-explained + +## 資料 - Slides + + diff --git a/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/featured.jpg b/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/featured.jpg new file mode 100644 index 0000000000..8a41448855 Binary files /dev/null and b/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/featured.jpg differ diff --git a/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/index.md b/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/index.md new file mode 100644 index 0000000000..a15feb31e6 --- /dev/null +++ b/content/event/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey/index.md @@ -0,0 +1,42 @@ +--- +title: "[Journal Club] The Landscape of Agentic Reinforcement Learning for LLMs: A Survey" +event: "Journal Club" +event_url: "" +location: "Online" +address: + street: "" + city: "" + region: "" + postcode: "" + country: "" +summary: "LLM が試行錯誤から行動を学ぶ強化学習手法と課題を紹介" +abstract: "大規模言語モデル(LLM)に強化学習を組み合わせた「Agentic RL」は,自律的な意思決定や動的な環境適応能力により,人工知能の新たなフロンティアを切り開いています。本資料では,この急速に進化するAgentic RLの全体像を,最新の包括的サーベイ論文「Agentic Reinforcement Learning: A Survey(2025)」に基づき詳細に解説します。\n\n- 📝:https://arxiv.org/abs/2509.02547\n- 🐙:https://github.com/xhyumiracle/Awesome-AgenticLLM-RL-Papers" +date: "2026-04-01T12:00:00+09:00" +date_end: "2026-04-01T12:00:00+09:00" +all_day: false +publishDate: "2026-04-01T12:00:00+09:00" +authors: ["Shunsuke Kitada"] +tags: ["Journal Club", "Paper Reading", "LLM", "Reinforcement Learning"] +featured: false +image: + caption: "" + focal_point: "" + preview_only: false +links: [] +url_slides: "https://speakerdeck.com/shunk031/the-landscape-of-agentic-reinforcement-learning-for-llms-a-survey" +url_code: "" +url_pdf: "" +url_video: "" +slides: "" +projects: [] +--- +# Journal Club + +> 大規模言語モデル(LLM)に強化学習を組み合わせた「Agentic RL」は,自律的な意思決定や動的な環境適応能力により,人工知能の新たなフロンティアを切り開いています。本資料では,この急速に進化するAgentic RLの全体像を,最新の包括的サーベイ論文「Agentic Reinforcement Learning: A Survey(2025)」に基づき詳細に解説します。 +> +> - 📝:https://arxiv.org/abs/2509.02547 +> - 🐙:https://github.com/xhyumiracle/Awesome-AgenticLLM-RL-Papers + +## 資料 - Slides + + diff --git a/tests/test_hugo_event_intake.py b/tests/test_hugo_event_intake.py index 3222cc5359..2ebe3cb44f 100644 --- a/tests/test_hugo_event_intake.py +++ b/tests/test_hugo_event_intake.py @@ -130,7 +130,7 @@ def test_probe_follows_event_page_links_and_uses_jsonld_event_fields(tmp_path: P """ def fake_fetch_text(url: str): - if url == "https://example.com/events/pydata-tokyo-1": + if url == "https://pydata-tokyo.connpass.com/event/123456/": return module.RemoteResponse(url=url, content_type="text/html", text=event_html, data=event_html.encode()) if url == "https://speakerdeck.com/shunk031/deep-learning-for-the-impatient": return module.RemoteResponse(url=url, content_type="text/html", text=speakerdeck_html, data=speakerdeck_html.encode()) @@ -146,7 +146,7 @@ def fake_fetch_json(url: str): result = module.probe_event( repo_root, - inputs=["https://example.com/events/pydata-tokyo-1"], + inputs=["https://pydata-tokyo.connpass.com/event/123456/"], fetch_text_response=fake_fetch_text, fetch_json=fake_fetch_json, ) @@ -159,7 +159,10 @@ def fake_fetch_json(url: str): assert result.location == "Shibuya Stream" assert result.address["city"] == "Shibuya-ku" assert result.url_slides == "https://speakerdeck.com/shunk031/deep-learning-for-the-impatient" - assert result.event_url == "https://example.com/events/pydata-tokyo-1" + assert result.event_url == "https://pydata-tokyo.connpass.com/event/123456/" + assert result.links == [ + {"name": "Connpass", "url": "https://pydata-tokyo.connpass.com/event/123456/"} + ] assert result.featured_source_url == "https://files.speakerdeck.com/presentations/abc123/slide_0.jpg" assert result.unresolved_fields == [] @@ -172,7 +175,17 @@ def test_probe_infers_journal_club_and_report_tags() -> None: sources=[], explicit_kind="", ) == "Journal Club" + assert module.infer_kind( + notes=["Large Language Model Agent: A Survey on Methodology, Applications and Challenges"], + sources=[], + explicit_kind="", + ) == "Journal Club" assert module.default_tags_for_kind("Journal Club") == ["Journal Club", "Paper Reading"] + assert module.infer_kind( + notes=["ECCV 2026 参加報告会"], + sources=[], + explicit_kind="", + ) == "Report" assert module.render_title("Report", "ECCV 2026 速報") == "ECCV 2026 速報" assert module.render_title("Invited Talk", "Practical ML") == "[Invited Talk] Practical ML" @@ -236,6 +249,32 @@ def fake_download(url: str): assert (output_path.parent / "featured.jpg").read_bytes() == b"JPEGDATA" +def test_build_body_labels_event_url_by_source_and_deduplicates_links() -> None: + module = load_module() + spec = module.EventSpec( + slug="pydata-tokyo", + event="PyData Tokyo #1", + event_url="https://pydata-tokyo.connpass.com/event/123456/", + url_slides="https://speakerdeck.com/shunk031/practical-ml", + links=[ + { + "name": "Connpass", + "url": "https://pydata-tokyo.connpass.com/event/123456/", + } + ], + ) + + body = module.build_body_markdown( + spec, + description="Practical ML talk.", + speakerdeck_source=None, + ) + + assert "- [Connpass](https://pydata-tokyo.connpass.com/event/123456/)" in body + assert "- [Slides](https://speakerdeck.com/shunk031/practical-ml)" in body + assert body.count("https://pydata-tokyo.connpass.com/event/123456/") == 1 + + def test_write_event_generates_thumbnail_from_pdf(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: module = load_module() repo_root = tmp_path / "repo" diff --git a/tests/test_hugo_talks_sync.py b/tests/test_hugo_talks_sync.py new file mode 100644 index 0000000000..f90c82cbe0 --- /dev/null +++ b/tests/test_hugo_talks_sync.py @@ -0,0 +1,392 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT_PATH = ( + REPO_ROOT + / ".agents" + / "skills" + / "hugo-talks-sync" + / "scripts" + / "hugo_talks_sync.py" +) + + +def load_module(): + spec = importlib.util.spec_from_file_location("hugo_talks_sync", SCRIPT_PATH) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_parse_rss_feed_extracts_deck_and_description_urls() -> None: + module = load_module() + feed = """ + + + + [Event] Talk Title / Deck Title + See https://example.com/event/42/ and https://github.com/example/code. + + Sat, 18 Jul 2026 00:00:00 -0400 + https://speakerdeck.com/shunk031/deck-title + https://speakerdeck.com/shunk031/deck-title + + + +""" + + source = module.SourceConfig( + name="speakerdeck-shunk031", + adapter="speakerdeck-profile", + url="https://speakerdeck.com/shunk031.rss", + ) + entries = module.parse_feed_entries(feed, source) + + assert len(entries) == 1 + assert entries[0].source_name == "speakerdeck-shunk031" + assert entries[0].adapter == "speakerdeck-profile" + assert entries[0].source_url == "https://speakerdeck.com/shunk031/deck-title" + assert entries[0].image_url == "https://files.speakerdeck.com/preview.jpg" + assert entries[0].extracted_urls == [ + "https://example.com/event/42/", + "https://github.com/example/code", + ] + + +def test_audit_marks_existing_deck_and_keeps_missing_candidate( + tmp_path: Path, monkeypatch +) -> None: + module = load_module() + event_dir = tmp_path / "content" / "event" / "existing-talk" + event_dir.mkdir(parents=True) + (event_dir / "index.md").write_text( + """--- +title: Existing +url_slides: https://speakerdeck.com/shunk031/existing-talk +--- +body +""", + encoding="utf-8", + ) + feed = """ + + + + Existing Talk + + Sat, 18 Jul 2026 00:00:00 -0400 + https://speakerdeck.com/shunk031/existing-talk + https://speakerdeck.com/shunk031/existing-talk + + + Missing Talk + Event https://example.com/missing + Sun, 19 Jul 2026 00:00:00 -0400 + https://speakerdeck.com/shunk031/missing-talk + https://speakerdeck.com/shunk031/missing-talk + + + +""" + monkeypatch.setattr(module, "read_text_url", lambda _url: feed) + + result = module.audit( + tmp_path, + [ + module.SourceConfig( + name="speakerdeck-shunk031", + adapter="speakerdeck-profile", + url="https://speakerdeck.com/shunk031.rss", + ) + ], + ) + + assert result["existing_count"] == 1 + assert result["candidate_count"] == 1 + assert result["existing"][0]["existing_path"] == "content/event/existing-talk/index.md" + assert result["candidates"][0]["source_name"] == "speakerdeck-shunk031" + assert result["candidates"][0]["source_type"] == "speakerdeck-profile" + assert result["candidates"][0]["likely_kind"] == "" + assert result["candidates"][0]["suggested_tags"] == [] + assert result["candidates"][0]["source_url"] == "https://speakerdeck.com/shunk031/missing-talk" + assert result["candidates"][0]["suggested_intake_inputs"] == [ + "https://speakerdeck.com/shunk031/missing-talk", + "https://example.com/missing", + ] + + +def test_load_source_configs_supports_profile_and_generic_feed(tmp_path: Path) -> None: + module = load_module() + config_path = tmp_path / "sources.json" + config_path.write_text( + """{ + "sources": [ + { + "name": "speakerdeck-shunk031", + "type": "speakerdeck-profile", + "profile_url": "https://speakerdeck.com/shunk031" + }, + { + "name": "personal-talk-feed", + "type": "feed", + "url": "https://example.com/talks.atom" + } + ] +} +""", + encoding="utf-8", + ) + + sources = module.load_source_configs(config_path) + + assert sources == [ + module.SourceConfig( + name="speakerdeck-shunk031", + adapter="speakerdeck-profile", + url="https://speakerdeck.com/shunk031.rss", + options={"profile_url": "https://speakerdeck.com/shunk031"}, + ), + module.SourceConfig( + name="personal-talk-feed", + adapter="feed", + url="https://example.com/talks.atom", + ), + ] + + +def test_load_source_configs_preserves_explicit_adapter_and_options( + tmp_path: Path, +) -> None: + module = load_module() + config_path = tmp_path / "sources.json" + config_path.write_text( + """{ + "sources": [ + { + "name": "personal-talks", + "adapter": "atom", + "url": "https://example.com/talks.atom", + "options": { + "owner": "shunk031", + "selector": ".talk" + } + } + ] +} +""", + encoding="utf-8", + ) + + sources = module.load_source_configs(config_path) + + assert sources == [ + module.SourceConfig( + name="personal-talks", + adapter="atom", + url="https://example.com/talks.atom", + options={"owner": "shunk031", "selector": ".talk"}, + ) + ] + + +def test_default_sources_use_requested_profile_name_and_feed_url() -> None: + module = load_module() + + sources = module.default_sources("https://speakerdeck.com/example-user") + + assert sources == [ + module.SourceConfig( + name="speakerdeck-com-example-user", + adapter="speakerdeck-profile", + url="https://speakerdeck.com/example-user.rss", + options={"profile_url": "https://speakerdeck.com/example-user"}, + ) + ] + + +def test_explicit_rss_adapter_rejects_atom_xml() -> None: + module = load_module() + source = module.SourceConfig( + name="rss-source", + adapter="rss", + url="https://example.com/talks.rss", + ) + atom_feed = """ + + Atom Talk + +""" + + try: + module.parse_feed_entries(atom_feed, source) + except ValueError as exc: + assert "expected RSS XML" in str(exc) + else: + raise AssertionError("explicit rss adapter accepted Atom XML") + + +def test_unknown_auto_feed_xml_fails_closed() -> None: + module = load_module() + source = module.SourceConfig( + name="unknown-source", + adapter="feed", + url="https://example.com/feed.xml", + ) + + try: + module.parse_feed_entries("", source) + except ValueError as exc: + assert "neither RSS nor Atom" in str(exc) + else: + raise AssertionError("unknown XML root was treated as an empty feed") + + +def test_audit_supports_multiple_source_adapters(tmp_path: Path, monkeypatch) -> None: + module = load_module() + (tmp_path / "content" / "event").mkdir(parents=True) + rss_feed = """ + + + + RSS Talk + Event https://example.com/rss-event + Sun, 19 Jul 2026 00:00:00 -0400 + https://slides.example.com/rss-talk + https://slides.example.com/rss-talk + + + +""" + atom_feed = """ + + + Atom Talk + + 2026-07-20T00:00:00Z + Video https://example.com/atom-event + + +""" + feeds = { + "https://example.com/talks.rss": rss_feed, + "https://example.com/talks.atom": atom_feed, + } + monkeypatch.setattr(module, "read_text_url", lambda url: feeds[url]) + + result = module.audit( + tmp_path, + [ + module.SourceConfig( + name="rss-source", + adapter="feed", + url="https://example.com/talks.rss", + ), + module.SourceConfig( + name="atom-source", + adapter="feed", + url="https://example.com/talks.atom", + ), + ], + ) + + assert result["candidate_count"] == 2 + assert [ + (item["source_name"], item["source_type"], item["source_url"]) + for item in result["candidates"] + ] == [ + ("rss-source", "feed", "https://slides.example.com/rss-talk"), + ("atom-source", "feed", "https://video.example.com/atom-talk"), + ] + + +def test_audit_deduplicates_same_candidate_across_sources( + tmp_path: Path, monkeypatch +) -> None: + module = load_module() + (tmp_path / "content" / "event").mkdir(parents=True) + rss_feed = """ + + + + Shared Talk + + Sun, 19 Jul 2026 00:00:00 -0400 + https://slides.example.com/shared-talk + https://slides.example.com/shared-talk + + + +""" + feeds = { + "https://example.com/one.rss": rss_feed, + "https://example.com/two.rss": rss_feed, + } + monkeypatch.setattr(module, "read_text_url", lambda url: feeds[url]) + + result = module.audit( + tmp_path, + [ + module.SourceConfig( + name="first", + adapter="rss", + url="https://example.com/one.rss", + ), + module.SourceConfig( + name="second", + adapter="rss", + url="https://example.com/two.rss", + ), + ], + ) + + assert result["candidate_count"] == 1 + assert result["duplicate_count"] == 1 + assert result["duplicates"][0]["source_name"] == "second" + assert result["duplicates"][0]["duplicate_of"] == "https://slides.example.com/shared-talk" + + +def test_audit_suggests_journal_club_for_paper_reading_candidate( + tmp_path: Path, monkeypatch +) -> None: + module = load_module() + (tmp_path / "content" / "event").mkdir(parents=True) + feed = """ + + + + Large Language Model Agent: A Survey on Methodology, Applications and Challenges + Paper notes https://arxiv.org/abs/2503.21460 + Sun, 19 Jul 2026 00:00:00 -0400 + https://speakerdeck.com/shunk031/llm-agent-survey + https://speakerdeck.com/shunk031/llm-agent-survey + + + +""" + monkeypatch.setattr(module, "read_text_url", lambda _url: feed) + + result = module.audit( + tmp_path, + [ + module.SourceConfig( + name="speakerdeck-shunk031", + adapter="speakerdeck-profile", + url="https://speakerdeck.com/shunk031.rss", + ) + ], + ) + + assert result["candidate_count"] == 1 + assert result["candidates"][0]["likely_kind"] == "Journal Club" + assert result["candidates"][0]["suggested_tags"] == [ + "Journal Club", + "Paper Reading", + ]