From edd9630d310152bbf54aea943e9d7920c52b8329 Mon Sep 17 00:00:00 2001 From: Corey Pyle Date: Thu, 3 Jul 2025 11:02:52 -0400 Subject: [PATCH 1/2] Add FS utility. --- aws_doc_sdk_examples_tools/fs.py | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 aws_doc_sdk_examples_tools/fs.py diff --git a/aws_doc_sdk_examples_tools/fs.py b/aws_doc_sdk_examples_tools/fs.py new file mode 100644 index 0000000..e980e60 --- /dev/null +++ b/aws_doc_sdk_examples_tools/fs.py @@ -0,0 +1,112 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from fnmatch import fnmatch +from os import listdir +from pathlib import Path +from stat import S_ISREG +from typing import Dict, Generator, List + + +@dataclass(frozen=True) +class Stat: + path: Path + exists: bool + is_file: bool + + @property + def is_dir(self): + return self.exists and not self.is_file + + +class Fs(ABC): + @abstractmethod + def glob(self, path: Path, glob: str) -> Generator[Path, None, None]: + pass + + @abstractmethod + def read(self, path: Path) -> str: + pass + + @abstractmethod + def write(self, path: Path, content: str): + pass + + @abstractmethod + def stat(self, path: Path) -> Stat: + pass + + @abstractmethod + def mkdir(self, path: Path): + pass + + @abstractmethod + def list(self, path: Path) -> List[Path]: + pass + + +class PathFs(Fs): + def glob(self, path: Path, glob: str) -> Generator[Path, None, None]: + return path.glob(glob) + + def read(self, path: Path) -> str: + with path.open("r") as file: + return file.read() + + def write(self, path: Path, content: str): + with path.open("w") as file: + file.write(content) + + def stat(self, path: Path) -> Stat: + if path.exists(): + stat = path.stat() + return Stat(path, True, S_ISREG(stat.st_mode)) + else: + return Stat(path, False, False) + + def mkdir(self, path: Path): + path.mkdir(parents=True, exist_ok=True) + + def list(self, path: Path) -> List[Path]: + if self.stat(path).is_file: + return [] + return [path / name for name in listdir(path)] + + +class RecordFs(Fs): + def __init__(self, fs: Dict[Path, str]): + self.fs = fs + + def glob(self, path: Path, glob: str) -> Generator[Path, None, None]: + path_s = str(path) + for key in self.fs.keys(): + key_s = str(key) + if key_s.startswith(path_s): + if fnmatch(key_s, glob): + yield key + + def read(self, path: Path) -> str: + return self.fs[path] + + def write(self, path: Path, content: str): + base = str(path.parent) + assert any( + [str(key).startswith(base) for key in self.fs] + ), "No parent folder, this will probably fail without a call to mkdir in a real file system!" + self.fs[path] = content + + def stat(self, path: Path): + if path in self.fs: + return Stat(path, True, True) + for item in self.fs.keys(): + if str(item).startswith(str(path)): + return Stat(path, True, False) + return Stat(path, False, False) + + def mkdir(self, path: Path): + self.fs.setdefault(path, "") + + def list(self, path: Path) -> List[Path]: + return [item for item in self.fs.keys() if item.parent == path] + + +fs = PathFs() From 75f467885623269fb4bde30d7fd90de1d48fa922 Mon Sep 17 00:00:00 2001 From: Corey Pyle Date: Thu, 3 Jul 2025 11:08:52 -0400 Subject: [PATCH 2/2] Add Lliam domain --- aws_doc_sdk_examples_tools/lliam/README.md | 78 +++++++++++++++++++ .../lliam/domain/commands.py | 26 +++++++ .../lliam/domain/model.py | 15 ++++ .../lliam/domain/operations.py | 28 +++++++ 4 files changed, 147 insertions(+) create mode 100644 aws_doc_sdk_examples_tools/lliam/README.md create mode 100644 aws_doc_sdk_examples_tools/lliam/domain/commands.py create mode 100644 aws_doc_sdk_examples_tools/lliam/domain/model.py create mode 100644 aws_doc_sdk_examples_tools/lliam/domain/operations.py diff --git a/aws_doc_sdk_examples_tools/lliam/README.md b/aws_doc_sdk_examples_tools/lliam/README.md new file mode 100644 index 0000000..6955f13 --- /dev/null +++ b/aws_doc_sdk_examples_tools/lliam/README.md @@ -0,0 +1,78 @@ +# Ailly Prompt Workflow + +This project automates the process of generating, running, parsing, and applying [Ailly](https://www.npmjs.com/package/@ailly/cli) prompt outputs to an AWS DocGen project. It combines all steps into one streamlined command using a single Python script. + +--- + +## 📦 Overview + +This tool: +1. **Generates** Ailly prompts from DocGen snippets. +2. **Runs** Ailly CLI to get enhanced metadata. +3. **Parses** Ailly responses into structured JSON. +4. **Updates** your DocGen examples with the new metadata. + +All of this is done with one command. + +--- + +## ✅ Prerequisites + +- Python 3.8+ +- Node.js and npm (for `npx`) +- A DocGen project directory + +--- + +## 🚀 Usage + +From your project root, run: + +```bash +python -m aws_doc_sdk_examples_tools.agent.bin.main \ + /path/to/your/docgen/project \ + --system-prompts path/to/system_prompt.txt +``` + +### 🔧 Arguments + +- `iam_tributary_root`: Path to the root directory of your IAM policy tributary +- `--system-prompts`: List of system prompt files or strings to include in the Ailly configuration +- `--skip-generation`: Skip the prompt generation and Ailly execution steps (useful for reprocessing existing outputs) + +Run `python -m aws_doc_sdk_examples_tools.agent.bin.main update --help` for more info. + +--- + +## 🗂 What This Does + +Under the hood, this script: + +1. Creates a directory `.ailly_iam_policy` containing: + - One Markdown file per snippet. + - A `.aillyrc` configuration file. + +2. Runs `npx @ailly/cli` to generate `.ailly.md` outputs. + +3. Parses the Ailly `.ailly.md` files into a single `iam_updates.json` file. + +4. Updates each matching `Example` in the DocGen instance with: + - `title` + - `title_abbrev` + - `synopsis` + +--- + +## 💡 Example + +```bash +python -m aws_doc_sdk_examples_tools.agent.bin.main \ + ~/projects/AWSIAMPolicyExampleReservoir \ + --system-prompts prompts/system_prompt.txt +``` + +This will: +- Write prompts and config to `.ailly_iam_policy/` +- Run Ailly and capture results +- Parse and save output as `.ailly_iam_policy/iam_updates.json` +- Apply updates to your DocGen examples diff --git a/aws_doc_sdk_examples_tools/lliam/domain/commands.py b/aws_doc_sdk_examples_tools/lliam/domain/commands.py new file mode 100644 index 0000000..b6e14e4 --- /dev/null +++ b/aws_doc_sdk_examples_tools/lliam/domain/commands.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import List + + +class Command: + pass + + +@dataclass +class CreatePrompts(Command): + doc_gen_root: str + system_prompts: List[str] + out_dir: str + + +@dataclass +class RunAilly(Command): + batches: List[str] + + +@dataclass +class UpdateReservoir(Command): + root: Path + batches: List[str] + packages: List[str] diff --git a/aws_doc_sdk_examples_tools/lliam/domain/model.py b/aws_doc_sdk_examples_tools/lliam/domain/model.py new file mode 100644 index 0000000..33c8548 --- /dev/null +++ b/aws_doc_sdk_examples_tools/lliam/domain/model.py @@ -0,0 +1,15 @@ +from typing import List + +from aws_doc_sdk_examples_tools.doc_gen import Example, Snippet + + +class Prompt: + def __init__(self, id: str, content: str): + self.id = id + self.content = content + + +class Policies: + def __init__(self, examples: List[Example], snippets: List[Snippet]): + self.examples = examples + self.snippets = snippets diff --git a/aws_doc_sdk_examples_tools/lliam/domain/operations.py b/aws_doc_sdk_examples_tools/lliam/domain/operations.py new file mode 100644 index 0000000..fd816b4 --- /dev/null +++ b/aws_doc_sdk_examples_tools/lliam/domain/operations.py @@ -0,0 +1,28 @@ +import yaml +from typing import List + +from aws_doc_sdk_examples_tools.lliam.domain.model import Prompt + + +def build_ailly_config(system_prompts: List[Prompt]) -> Prompt: + """Create the .aillyrc configuration file.""" + fence = "---" + options = { + "isolated": "true", + "overwrite": "true", + # MCP assistance did not produce noticeably different results, but it was + # slowing things down by 10x. Disabled for now. + # "mcp": { + # "awslabs.aws-documentation-mcp-server": { + # "type": "stdio", + # "command": "uvx", + # "args": ["awslabs.aws-documentation-mcp-server@latest"], + # } + # }, + } + options_block = yaml.dump(options).strip() + prompt_strs = [p.content for p in system_prompts] + prompts_block = "\n".join(prompt_strs) + + content = f"{fence}\n{options_block}\n{fence}\n{prompts_block}" + return Prompt(".aillyrc", content)