diff --git a/Makefile b/Makefile index f1170a349c..a0a3c75c30 100644 --- a/Makefile +++ b/Makefile @@ -257,3 +257,11 @@ docker-run-raphtory-arm64: $(if $(OTLP_AGENT_HOST),--otlp-agent-host=$(OTLP_AGENT_HOST)) \ $(if $(OTLP_AGENT_PORT),--otlp-agent-port=$(OTLP_AGENT_PORT)) \ $(if $(OTLP_TRACING_SERVICE_NAME),--otlp-tracing-service-name=$(OTLP_TRACING_SERVICE_NAME)) + +################ +# Claude Skill # +################ + +claude-skill: build-python-public + cat llms/generate-raphtory-skill.md | claude --permission-mode dontAsk --allowedTools 'Bash,Read,Write,Glob,Grep' + python llms/build_skill_md.py diff --git a/llms/SKILL.md b/llms/SKILL.md new file mode 100644 index 0000000000..cf2ebd76ce --- /dev/null +++ b/llms/SKILL.md @@ -0,0 +1,42 @@ +--- +name: raphtory-python-api +description: > + Use this skill when the user asks to write Python code using raphtory, + create temporal graphs, run graph algorithms, query graph properties, + use time views or layers, load graph data, or work with the raphtory + Python library in any way. Also use when discussing raphtory API usage, + graph analytics, or temporal network analysis with raphtory. +version: 0.17.0 +--- + +# Raphtory Python API Examples + + +## Graph Creation & Basics +```python +from raphtory import Graph +``` + + +```python +g = Graph() +print(g) +# => Graph(number_of_nodes=0, number_of_edges=0, number_of_temporal_edges=0, earliest_time=None, latest_time=None) +``` + + +# Add nodes with timestamps and properties +```python +g.add_node(1, "alice", properties={"age": 30}) +g.add_node(2, "bob", properties={"age": 25}) +print(g.count_nodes()) +# => 2 +``` + + +# Add edges with layers +```python +g.add_edge(3, "alice", "bob", properties={"weight": 1.0}, layer="friends") +print(g.count_edges()) +# => 1 +``` diff --git a/llms/build_skill_md.py b/llms/build_skill_md.py new file mode 100644 index 0000000000..18658fcecb --- /dev/null +++ b/llms/build_skill_md.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Build SKILL.md from raphtory_python_examples.py by executing the script +line-by-line and interleaving code with captured output. + +Usage: python3 scripts/build_skill_md.py +""" + +import ast +import io +import contextlib +import subprocess +import sys +from pathlib import Path + +SCRIPT_PATH = Path(__file__).parent / "raphtory_python_examples.py" +OUTPUT_PATH = Path(__file__).parent / "SKILL.md" + +FRONTMATTER = """\ +--- +name: raphtory-python-api +description: > + Use this skill when the user asks to write Python code using raphtory, + create temporal graphs, run graph algorithms, query graph properties, + use time views or layers, load graph data, or work with the raphtory + Python library in any way. Also use when discussing raphtory API usage, + graph analytics, or temporal network analysis with raphtory. +version: {version} +--- + +# Raphtory Python API Examples + +""" + + +def get_version(): + result = subprocess.run( + [sys.executable, "-c", "import raphtory; print(raphtory.__version__)"], + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def build_skill(): + version = get_version() + source = SCRIPT_PATH.read_text() + lines = source.splitlines() + + # Parse the script into blocks: comments, blank lines, and code chunks. + # A code chunk is one or more consecutive non-comment, non-blank lines + # that form a complete statement (or group of statements). + blocks = [] # each block is ("comment", text) | ("blank",) | ("code", lines_text) + i = 0 + while i < len(lines): + line = lines[i] + if line.strip() == "": + blocks.append(("blank",)) + i += 1 + elif line.lstrip().startswith("#"): + blocks.append(("comment", line)) + i += 1 + else: + # Collect consecutive code lines until we hit a blank or comment + code_lines = [] + while i < len(lines) and lines[i].strip() != "" and not ( + lines[i].lstrip().startswith("#") and not code_lines_need_continuation(code_lines) + ): + code_lines.append(lines[i]) + i += 1 + blocks.append(("code", "\n".join(code_lines))) + + # Now execute code blocks incrementally and capture output + namespace = {} + md_lines = [FRONTMATTER.format(version=version)] + in_code_block = False + + for block in blocks: + if block[0] == "blank": + if in_code_block: + md_lines.append("```\n") + in_code_block = False + md_lines.append("") + elif block[0] == "comment": + if in_code_block: + md_lines.append("```\n") + in_code_block = False + text = block[1] + if text.startswith("## "): + # Section header + md_lines.append(text) + else: + md_lines.append(text) + elif block[0] == "code": + code_text = block[1] + if not in_code_block: + md_lines.append("```python") + in_code_block = True + + # Capture stdout from executing this code block + stdout_capture = io.StringIO() + try: + with contextlib.redirect_stdout(stdout_capture): + exec(compile(code_text, SCRIPT_PATH, "exec"), namespace) + except Exception as e: + print(f"Error executing:\n{code_text}\n\nException: {e}", file=sys.stderr) + sys.exit(1) + + captured = stdout_capture.getvalue() + + # Add code lines to markdown + for code_line in code_text.splitlines(): + md_lines.append(code_line) + + # Add captured output as comments + if captured.strip(): + for out_line in captured.strip().splitlines(): + md_lines.append(f"# => {out_line}") + + if in_code_block: + md_lines.append("```\n") + + OUTPUT_PATH.write_text("\n".join(md_lines)) + print(f"Written {OUTPUT_PATH} ({len(md_lines)} lines, version {version})") + + +def code_lines_need_continuation(code_lines): + """Check if the accumulated code lines are an incomplete statement.""" + if not code_lines: + return False + text = "\n".join(code_lines) + try: + ast.parse(text) + return False + except SyntaxError: + return True + + +if __name__ == "__main__": + build_skill() diff --git a/llms/generate-raphtory-skill.md b/llms/generate-raphtory-skill.md new file mode 100644 index 0000000000..d549d9a944 --- /dev/null +++ b/llms/generate-raphtory-skill.md @@ -0,0 +1,105 @@ +# Generate Raphtory Python API Examples + +You are generating a Python script that demonstrates the entire raphtory Python API through compact, runnable examples. This script will later be converted into a Claude Code skill file automatically. + +## Step 1: Discover the API surface + +First, get the installed version: + +```bash +python3 -c "import raphtory; print(raphtory.__version__)" +``` + +Then dynamically discover all raphtory submodules and read their pydoc: + +```bash +python3 -c " +import raphtory, pkgutil, importlib +modules = ['raphtory'] +for importer, modname, ispkg in pkgutil.walk_packages(raphtory.__path__, prefix='raphtory.'): + if not modname.startswith('raphtory._'): + modules.append(modname) +print('\n'.join(modules)) +" +``` + +Run `pydoc ` for the top-level `raphtory` module and every discovered submodule. Read through ALL of them carefully — this is the authoritative source for the current API. + +## Step 2: Generate the examples script + +Write the file `llms/raphtory_python_examples.py`. This is a plain Python script where: + +- **Section headers** are comments starting with `## ` (e.g. `## Graph Creation & Basics`) +- **Explanations** are regular `#` comments +- **Code** is normal Python statements +- Lines that produce meaningful output should use `print()` so the output can be captured later +- The script must run top-to-bottom without errors + +### Topics to cover (adapt based on what actually exists in the API): + +- Graph creation and basic metrics +- Adding nodes and edges (with timestamps, properties, types, layers) +- Bulk loading from DataFrames, dicts, CSV +- Querying nodes and edges (iteration, properties, temporal properties, history) +- Time views and windowing +- Layer views +- Subgraph and filtering +- ALL algorithms (grouped by category) +- Graph generators and built-in dataset loaders +- GraphQL server basics (import and setup, but don't actually start the server) +- Saving and loading graphs + +### Style rules + +- Keep the script to ~300-500 lines +- Use comments for section headers and brief explanations only — no walls of text +- Group related operations together +- Use built-in loaders (e.g. `raphtory.graph_loader.karate_club_graph()`) or simple hand-built graphs — no external CSV files +- Every algorithm should have at least a one-line example +- Use `print()` for outputs you want to show (e.g. `print(g.count_nodes())`) +- Do NOT wrap everything in functions or classes — keep it as a flat script + +### Example of expected style: + +```python +## Graph Creation & Basics +from raphtory import Graph + +g = Graph() +print(g) + +# Add nodes with timestamps and properties +g.add_node(1, "alice", properties={"age": 30}) +g.add_node(2, "bob", properties={"age": 25}) +print(g.count_nodes()) + +# Add edges with layers +g.add_edge(3, "alice", "bob", properties={"weight": 1.0}, layer="friends") +print(g.count_edges()) +``` + +## Step 3: Verify the script + +**This is critical.** Run the entire script and confirm it executes without errors: + +```bash +python llms/raphtory_python_examples.py +``` + +If any part fails: + +1. Debug why it failed +2. Fix the script +3. Re-run to verify + +## Step 4: Final review + +Before writing the final file, verify: + +- [ ] All major API areas discovered in Step 1 are covered +- [ ] All algorithms have at least a one-line example +- [ ] The entire script executes successfully end-to-end +- [ ] The script is ~300-500 lines +- [ ] No external data file dependencies + +Write the result to `llms/raphtory_python_examples.py` using Bash (do NOT use the Write tool). diff --git a/llms/raphtory_python_examples.py b/llms/raphtory_python_examples.py new file mode 100644 index 0000000000..61b7332cf4 --- /dev/null +++ b/llms/raphtory_python_examples.py @@ -0,0 +1,14 @@ +## Graph Creation & Basics +from raphtory import Graph + +g = Graph() +print(g) + +# Add nodes with timestamps and properties +g.add_node(1, "alice", properties={"age": 30}) +g.add_node(2, "bob", properties={"age": 25}) +print(g.count_nodes()) + +# Add edges with layers +g.add_edge(3, "alice", "bob", properties={"weight": 1.0}, layer="friends") +print(g.count_edges()) diff --git a/raphtory-graphql/src/cli.rs b/raphtory-graphql/src/cli.rs index a0d67a517b..d00ddb3756 100644 --- a/raphtory-graphql/src/cli.rs +++ b/raphtory-graphql/src/cli.rs @@ -19,6 +19,8 @@ use clap::{command, Parser, Subcommand}; use std::path::PathBuf; use tokio::io::Result as IoResult; +const SKILL_CONTENT: &str = include_str!("../../llms/SKILL.md"); + #[derive(Parser)] #[command(name = "raphtory", about = "Raphtory CLI", version = raphtory::version())] struct Args { @@ -32,6 +34,8 @@ enum Commands { Server(ServerArgs), #[command(about = "Print the GraphQL schema")] Schema, + #[command(about = "Install Claude Code skill for the raphtory Python API")] + InstallSkill, } #[derive(clap::Args)] @@ -97,6 +101,21 @@ where let schema = App::create_schema().finish().unwrap(); println!("{}", schema.sdl()); } + Commands::InstallSkill => { + let home = std::env::var("HOME") + .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; + let skill_dir = PathBuf::from(home) + .join(".claude") + .join("skills") + .join("raphtory-python-api"); + std::fs::create_dir_all(&skill_dir)?; + let skill_path = skill_dir.join("SKILL.md"); + std::fs::write(&skill_path, SKILL_CONTENT)?; + println!( + "Installed raphtory Claude Code skill to {}", + skill_path.display() + ); + } Commands::Server(server_args) => { let mut builder = AppConfigBuilder::new() .with_cache_capacity(server_args.cache_capacity)