Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
---
name: aminer-academic-search
description: >
ACADEMIC PRIORITY: Activate whenever the user's query involves academic,
scholarly, or research-related topics — papers, citations, scholars,
institutions, venues, patents, research trends, or any "who published what /
where / when" question. Takes precedence over general web search for academic
data needs. Routes through the z-ai gateway's `/v1/functions/invoke` endpoint
to the AMiner Open Platform (27 APIs, 5 workflows).
---
# aminer-academic-search
Wraps 27 AMiner Open Platform APIs through the local gateway. The gateway owns
the `AMINER_API_KEY`; the skill only needs a `.z-ai-config` file to reach the
gateway.
## When to activate
Any academic/scholarly query — papers, citations, scholars, institutions,
venues, patents. Covers:
- Scholar full profile (bio, education, honors, papers, patents, projects)
- Paper deep dive (full abstract, keywords, authors, citation chain)
- Multi-condition or semantic paper search
- Institution research capability analysis
- Venue annual monitoring
- Patent deep details (IPC/CPC, assignee, claims)
## Config
Script reads `.z-ai-config`, in order:
1. `./.z-ai-config`
2. `~/.z-ai-config`
3. `/etc/.z-ai-config`
Required fields: `baseUrl`, `apiKey`, `token` (JWT). Optional: `chatId`,
`userId`.
## Invocation
```bash
python3 "{baseDir}/scripts/aminer.py" <ACTION> [--flag value …]
```
The script POSTs to `${baseUrl}/functions/invoke` with
`Authorization: Bearer ${apiKey}`, `X-Z-AI-From: Z`, `X-Token: ${token}`,
and prints the upstream JSON result (pretty-formatted).
Run `python3 scripts/aminer.py --help` to list actions, or
`python3 scripts/aminer.py <ACTION> --help` for per-action flags.
### Examples
```bash
# Locate paper_id by title
python3 scripts/aminer.py paper_search --title "Attention is all you need"
# Locate scholar by name + org
python3 scripts/aminer.py person_search --name "Jie Tang" --org "Tsinghua"
# Full paper details
python3 scripts/aminer.py paper_detail --id 57a4e91aac44365e35c98a1e
# Natural-language Q&A search
python3 scripts/aminer.py paper_qa_search --query "retrieval-augmented generation" --size 5
# Venue papers for a given year (body with JSON list param)
python3 scripts/aminer.py venue_paper_relation --id "53e9ba63b7602d9702f08c5d" --year 2024 --limit 10
```
JSON-typed flags (`--title`, `--ids`, `--keywords`, `--year` on qa_search, etc.)
expect a JSON-encoded value, e.g. `--ids '["abc","def"]'`.
## 27 APIs (quick reference)
| Action | Method | Purpose |
|---|---|---|
| paper_search | GET | Locate paper_id by title |
| paper_search_pro | GET | Multi-condition paper search |
| paper_qa_search | POST | Natural-language / topic Q&A |
| paper_info | POST | Batch paper info by IDs |
| paper_detail | GET | Full paper details |
| paper_relation | GET | Citation chain |
| paper_list_by_keywords | GET | Batch thematic retrieval |
| paper_detail_by_condition | GET | Year + venue dimension |
| person_search | POST | Locate person_id |
| person_detail | GET | Scholar bio/education/honors |
| person_figure | GET | Interests + work history |
| person_paper_relation | GET | Scholar's papers |
| person_patent_relation | GET | Scholar's patents |
| person_project | GET | Funded projects |
| org_search | POST | Locate org by name |
| org_detail | POST | Org description/type |
| org_person_relation | GET | Affiliated scholars |
| org_paper_relation | GET | Org papers |
| org_patent_relation | GET | Org patents |
| org_disambiguate | POST | Normalize org string |
| org_disambiguate_pro | POST | Extract org IDs |
| venue_search | POST | Locate venue_id |
| venue_detail | POST | ISSN/type/abbreviation |
| venue_paper_relation | POST | Papers by venue (+ year filter) |
| patent_search | POST | Patent keyword search |
| patent_info | GET | Basic patent info |
| patent_detail | GET | Full patent details (IPC/claims) |
## 5 Workflows (orchestrate via multiple calls)
### Scholar Profile
```
person_search → person_detail
→ person_figure
→ person_paper_relation
→ person_patent_relation
→ person_project
```
### Paper Deep Dive
```
paper_search → paper_detail → paper_relation → paper_info
```
If `paper_search` is empty, fall back to `paper_search_pro`.
### Org Analysis
```
org_disambiguate_pro → org_detail
→ org_person_relation
→ org_paper_relation
→ org_patent_relation
```
If `org_disambiguate_pro` returns no ID, fall back to `org_search`.
### Venue Papers
```
venue_search → venue_detail (optional) → venue_paper_relation
```
### Patent Analysis
```
patent_search → patent_info / patent_detail
```
## Error handling
- `warning: .z-ai-config has no 'token' field` → add `token` to config.
- `http 401 … missing X-Token header (hint: …)` → same.
- `http 403` → gateway auth rejected; check `apiKey` / `X-Z-AI-From`.
- `gateway error: aminer_* failed: http 4xx …` → upstream refused; check API
key on the gateway side (`AMINER_API_KEY` env var) or parameter shape.
## Entity URL templates
- Paper: `https://www.aminer.cn/pub/{paper_id}`
- Scholar: `https://www.aminer.cn/profile/{scholar_id}`
- Patent: `https://www.aminer.cn/patent/{patent_id}`
- Journal: `https://www.aminer.cn/open/journal/detail/{journal_id}`
Always append the relevant URL when presenting entities to the user.

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""Dispatcher for the 27 AMiner Open Platform functions exposed via the
z-ai gateway's /v1/functions/invoke endpoint.
Each action maps 1:1 to an `aminer_*` function registered in the gateway.
Arguments are collected via argparse and forwarded as the `arguments` field.
Upstream response is printed as formatted JSON.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import sys
import urllib.error
import urllib.request
from typing import Any, Callable, Tuple
REQUEST_TIMEOUT = 60
CONFIG_PATHS = [
pathlib.Path.cwd() / ".z-ai-config",
pathlib.Path.home() / ".z-ai-config",
pathlib.Path("/etc/.z-ai-config"),
]
# ---------------------------------------------------------------------------
# Action registry
#
# Each entry defines one AMiner Open Platform API:
# function_name: matches the key in handlers/aminer_open.go's AminerOpenSpecs
# args: (flag, dest, kind, help) — kind is "str"/"int"/"float"/
# "bool"/"json" (parsed from JSON for list/dict/nested values)
#
# This list is the *single source of truth* for what params each action takes,
# so the model only has to read one structure to learn the whole surface.
# ---------------------------------------------------------------------------
_Arg = Tuple[str, str, str, str] # (flag, dest, kind, help)
ACTIONS: dict[str, dict[str, Any]] = {
# ── Paper ────────────────────────────────────────────────────────────
"paper_search": {
"function": "aminer_paper_search",
"help": "Locate a paper_id by (partial) title.",
"args": [
("--title", "title", "str", "Paper title keyword (required)"),
("--page", "page", "int", "Page number, default 1"),
("--size", "size", "int", "Page size, default 10"),
],
},
"paper_search_pro": {
"function": "aminer_paper_search_pro",
"help": "Multi-condition paper search (author/org/venue/keyword).",
"args": [
("--title", "title", "str", "Title keyword"),
("--keyword", "keyword", "str", "Subject keyword"),
("--abstract", "abstract", "str", "Abstract keyword"),
("--author", "author", "str", "Author name"),
("--org", "org", "str", "Organization name"),
("--venue", "venue", "str", "Venue name"),
("--order", "order", "str", "Sort: citation | year"),
("--page", "page", "int", "Page number, default 0"),
("--size", "size", "int", "Page size, default 10"),
],
},
"paper_qa_search": {
"function": "aminer_paper_qa_search",
"help": "AI Q&A-style search. 'query' and 'topic_*' are mutually exclusive.",
"args": [
("--query", "query", "str", "Natural language question"),
("--use-topic", "use_topic", "bool", "Use topic_* fields instead of query"),
("--topic-high", "topic_high", "str", "High-level topic"),
("--topic-middle", "topic_middle", "str", "Mid-level topic"),
("--topic-low", "topic_low", "str", "Low-level topic"),
("--title", "title", "json", "List of titles (JSON array)"),
("--doi", "doi", "str", "DOI"),
("--year", "year", "json", "Year filter, e.g. [2020,2024]"),
("--sci-flag", "sci_flag", "bool", "Restrict to SCI venues"),
("--n-citation-flag", "n_citation_flag", "bool", "Return citation count"),
("--force-citation-sort", "force_citation_sort", "bool", "Sort by citations"),
("--force-year-sort", "force_year_sort", "bool", "Sort by year"),
("--author-terms", "author_terms", "json", "Author name list"),
("--org-terms", "org_terms", "json", "Org name list"),
("--author-id", "author_id", "json", "Author ID list"),
("--org-id", "org_id", "json", "Org ID list"),
("--venue-ids", "venue_ids", "json", "Venue ID list"),
("--size", "size", "int", "Page size, default 10"),
("--offset", "offset", "int", "Offset, default 0"),
],
},
"paper_info": {
"function": "aminer_paper_info",
"help": "Batch retrieve papers by ID list.",
"args": [
("--ids", "ids", "json", "JSON array of paper_id strings (required)"),
],
},
"paper_detail": {
"function": "aminer_paper_detail",
"help": "Full paper info for a single paper_id.",
"args": [
("--id", "id", "str", "paper_id (required)"),
],
},
"paper_relation": {
"function": "aminer_paper_relation",
"help": "Citation chain (cited papers) for a paper_id.",
"args": [
("--id", "id", "str", "paper_id (required)"),
],
},
"paper_list_by_keywords": {
"function": "aminer_paper_list_by_keywords",
"help": "Batch keyword retrieval returning abstracts + metadata.",
"args": [
("--keywords", "keywords", "json", "JSON array of keyword strings (required)"),
("--page", "page", "int", "Page number, default 0"),
("--size", "size", "int", "Page size, default 10"),
],
},
"paper_detail_by_condition": {
"function": "aminer_paper_detail_by_condition",
"help": "Year + venue dimension lookup. Year + venue_id both required.",
"args": [
("--year", "year", "int", "Year (required)"),
("--venue-id", "venue_id", "str", "Venue ID (required)"),
],
},
# ── Scholar ──────────────────────────────────────────────────────────
"person_search": {
"function": "aminer_person_search",
"help": "Search scholars by name and/or org.",
"args": [
("--name", "name", "str", "Scholar name"),
("--org", "org", "str", "Organization name"),
("--org-id", "org_id", "json", "List of org IDs"),
("--offset", "offset", "int", "Offset, default 0"),
("--size", "size", "int", "Page size, default 5"),
],
},
"person_detail": {
"function": "aminer_person_detail",
"help": "Full scholar profile (bio/education/honors).",
"args": [("--id", "id", "str", "person_id (required)")],
},
"person_figure": {
"function": "aminer_person_figure",
"help": "Scholar portrait (interests, work history).",
"args": [("--id", "id", "str", "person_id (required)")],
},
"person_paper_relation": {
"function": "aminer_person_paper_relation",
"help": "List of papers by this scholar.",
"args": [("--id", "id", "str", "person_id (required)")],
},
"person_patent_relation": {
"function": "aminer_person_patent_relation",
"help": "List of patents by this scholar.",
"args": [("--id", "id", "str", "person_id (required)")],
},
"person_project": {
"function": "aminer_person_project",
"help": "Research projects (funding, dates, source).",
"args": [("--id", "id", "str", "person_id (required)")],
},
# ── Organization ─────────────────────────────────────────────────────
"org_search": {
"function": "aminer_org_search",
"help": "Search institutions by name keyword.",
"args": [("--orgs", "orgs", "json", "JSON array of org name strings (required)")],
},
"org_detail": {
"function": "aminer_org_detail",
"help": "Org details by ID list.",
"args": [("--ids", "ids", "json", "JSON array of org_id strings (required)")],
},
"org_person_relation": {
"function": "aminer_org_person_relation",
"help": "Affiliated scholars (10 per call).",
"args": [
("--org-id", "org_id", "str", "org_id (required)"),
("--offset", "offset", "int", "Offset, default 0"),
],
},
"org_paper_relation": {
"function": "aminer_org_paper_relation",
"help": "Papers authored by org members (10 per call).",
"args": [
("--org-id", "org_id", "str", "org_id (required)"),
("--offset", "offset", "int", "Offset, default 0"),
],
},
"org_patent_relation": {
"function": "aminer_org_patent_relation",
"help": "Org patent list (max page_size 10,000).",
"args": [
("--id", "id", "str", "org_id (required)"),
("--page", "page", "int", "Page number, default 1"),
("--page-size", "page_size", "int", "Page size, default 100"),
],
},
"org_disambiguate": {
"function": "aminer_org_disambiguate",
"help": "Normalize raw org string.",
"args": [("--org", "org", "str", "Raw org string (required)")],
},
"org_disambiguate_pro": {
"function": "aminer_org_disambiguate_pro",
"help": "Extract primary/secondary org IDs.",
"args": [("--org", "org", "str", "Raw org string (required)")],
},
# ── Venue ────────────────────────────────────────────────────────────
"venue_search": {
"function": "aminer_venue_search",
"help": "Search journals/conferences by name.",
"args": [("--name", "name", "str", "Venue name (required)")],
},
"venue_detail": {
"function": "aminer_venue_detail",
"help": "Venue details (ISSN, abbreviation, type).",
"args": [("--id", "id", "str", "venue_id (required)")],
},
"venue_paper_relation": {
"function": "aminer_venue_paper_relation",
"help": "Papers published in a venue, optionally filtered by year.",
"args": [
("--id", "id", "str", "venue_id (required)"),
("--offset", "offset", "int", "Offset, default 0"),
("--limit", "limit", "int", "Limit, default 20"),
("--year", "year", "int", "Publication year"),
],
},
# ── Patent ───────────────────────────────────────────────────────────
"patent_search": {
"function": "aminer_patent_search",
"help": "Search patents by name/keyword.",
"args": [
("--query", "query", "str", "Search query (required)"),
("--page", "page", "int", "Page number, default 0"),
("--size", "size", "int", "Page size, default 10"),
],
},
"patent_info": {
"function": "aminer_patent_info",
"help": "Basic patent info.",
"args": [("--id", "id", "str", "patent_id (required)")],
},
"patent_detail": {
"function": "aminer_patent_detail",
"help": "Full patent details (abstract, IPC, claims).",
"args": [("--id", "id", "str", "patent_id (required)")],
},
}
# ---------------------------------------------------------------------------
# Config loading and HTTP
# ---------------------------------------------------------------------------
def load_config() -> dict[str, Any]:
for path in CONFIG_PATHS:
try:
cfg = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
except (OSError, json.JSONDecodeError) as exc:
raise SystemExit(f"failed to read {path}: {exc}") from exc
if cfg.get("baseUrl") and cfg.get("apiKey"):
if not cfg.get("token"):
print(
f"warning: {path} has no 'token' field; the gateway requires "
"X-Token for auth and will return 401.",
file=sys.stderr,
)
return cfg
raise SystemExit(
"z-ai config not found. Create .z-ai-config in cwd, $HOME, or /etc "
"with {\"baseUrl\": ..., \"apiKey\": ..., \"token\": ...}."
)
def invoke(config: dict[str, Any], function_name: str, arguments: dict[str, Any]) -> Any:
base_url = str(config["baseUrl"]).rstrip("/")
url = f"{base_url}/functions/invoke"
headers = {
"Content-Type": "application/json",
"User-Agent": "aminer-academic-search-skill/1.0",
"Authorization": f"Bearer {config['apiKey']}",
"X-Z-AI-From": "Z",
}
if config.get("token"):
headers["X-Token"] = str(config["token"])
if config.get("chatId"):
headers["X-Chat-Id"] = str(config["chatId"])
if config.get("userId"):
headers["X-User-Id"] = str(config["userId"])
body = json.dumps(
{"function_name": function_name, "arguments": arguments},
ensure_ascii=False,
).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: # nosec B310
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
hint = ""
if exc.code == 401 and "X-Token" in detail:
hint = " (hint: add a valid 'token' field to .z-ai-config)"
elif exc.code == 403:
hint = " (hint: request rejected by auth — check 'apiKey' / X-Z-AI-From)"
raise SystemExit(f"http {exc.code}: {detail or exc.reason}{hint}") from exc
except urllib.error.URLError as exc:
raise SystemExit(f"gateway unreachable: {exc.reason}") from exc
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
raise SystemExit(f"invalid json response: {exc}") from exc
if isinstance(parsed.get("error"), str) and parsed["error"]:
raise SystemExit(f"gateway error: {parsed['error']}")
return parsed.get("result")
# ---------------------------------------------------------------------------
# Argparse wiring
# ---------------------------------------------------------------------------
def _cast(kind: str) -> Callable[[str], Any]:
if kind == "int":
return int
if kind == "float":
return float
if kind == "json":
return lambda s: json.loads(s)
return str
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Call one of 27 AMiner Open Platform APIs via the z-ai gateway.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
sub = parser.add_subparsers(dest="action", required=True, metavar="ACTION")
for name, meta in ACTIONS.items():
p = sub.add_parser(
name,
help=meta["help"],
description=f"{meta['help']} function_name: {meta['function']}",
)
for flag, dest, kind, help_text in meta["args"]:
if kind == "bool":
p.add_argument(flag, dest=dest, action="store_true", help=help_text)
else:
p.add_argument(flag, dest=dest, type=_cast(kind), help=help_text)
return parser
def collect_arguments(ns: argparse.Namespace, spec: list[_Arg]) -> dict[str, Any]:
args: dict[str, Any] = {}
for _flag, dest, kind, _help in spec:
value = getattr(ns, dest, None)
if value is None:
continue
if kind == "bool" and value is False:
continue
args[dest] = value
return args
def main() -> int:
parser = build_parser()
ns = parser.parse_args()
meta = ACTIONS[ns.action]
arguments = collect_arguments(ns, meta["args"])
config = load_config()
result = invoke(config, meta["function"], arguments)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())