diff --git a/markitect/cli.py b/markitect/cli.py index 8e272eed..d675b18a 100644 --- a/markitect/cli.py +++ b/markitect/cli.py @@ -7095,6 +7095,13 @@ try: except ImportError: pass # Prompts module not available +# Register helper Q&A command +try: + from markitect.helper.cli import helper_command + cli.add_command(helper_command) +except ImportError: + pass # Helper module not available + # Make cli function available as main entry point main = cli diff --git a/markitect/helper/__init__.py b/markitect/helper/__init__.py new file mode 100644 index 00000000..8d6211f6 --- /dev/null +++ b/markitect/helper/__init__.py @@ -0,0 +1,6 @@ +""" +markitect.helper — Interactive Q&A helper for MarkiTect. + +Provides a CLI command that answers questions about markitect +using its own documentation as knowledge context. +""" diff --git a/markitect/helper/cli.py b/markitect/helper/cli.py new file mode 100644 index 00000000..769cee5a --- /dev/null +++ b/markitect/helper/cli.py @@ -0,0 +1,101 @@ +""" +CLI command for the markitect helper. +""" + +import os +import sys + +import click + +from markitect.helper.knowledge import collect_knowledge + +DEFAULT_PROVIDER = "openrouter" +DEFAULT_MODEL = "openrouter/aurora-alpha" +MODEL_ENV_VAR = "MARKITECT_HELPER_MODEL" + +SYSTEM_PROMPT_TEMPLATE = ( + "You are a MarkiTect expert assistant. Answer the user's question " + "based on the following MarkiTect documentation. Be concise and " + "accurate. If the documentation does not cover the question, say so.\n\n" + "{knowledge}" +) + + +@click.command("helper") +@click.argument("question", nargs=-1, required=True) +@click.option( + "--provider", "-p", + default=DEFAULT_PROVIDER, + type=click.Choice(["openrouter", "claude-code", "gemini", "openai"]), + show_default=True, + help="LLM provider to use.", +) +@click.option( + "--model", "-m", + default=None, + help=( + f"Model name. Overrides {MODEL_ENV_VAR} env var and the default " + f"({DEFAULT_MODEL})." + ), +) +def helper_command(question, provider, model): + """Ask a question about MarkiTect and get an answer from the docs. + + \b + Examples: + markitect helper "What is markitect?" + markitect helper How do schemas work + markitect helper -m anthropic/claude-sonnet-4 "Explain templates" + """ + from markitect.llm import create_adapter + from markitect.llm.exceptions import LLMConfigurationError, LLMError + from markitect.prompts.execution.models import RunConfig + + # Join multi-word question into a single string. + question_text = " ".join(question) + if not question_text.strip(): + click.echo("Error: empty question.", err=True) + sys.exit(1) + + # Resolve model: --model flag > env var > default. + resolved_model = model or os.environ.get(MODEL_ENV_VAR) or DEFAULT_MODEL + + # Build knowledge context. + click.echo("Loading markitect knowledge base...", err=True) + knowledge = collect_knowledge() + if not knowledge: + click.echo("Warning: no documentation files found.", err=True) + + system_prompt = SYSTEM_PROMPT_TEMPLATE.format(knowledge=knowledge) + + # Create adapter. + try: + adapter = create_adapter( + provider=provider, + model=resolved_model, + system_prompt=system_prompt, + ) + except LLMConfigurationError as exc: + click.echo(f"Configuration error: {exc}", err=True) + if "api" in str(exc).lower() or "key" in str(exc).lower(): + click.echo( + "Hint: set OPENROUTER_API_KEY (or the relevant provider key) " + "in your environment.", + err=True, + ) + sys.exit(1) + + # Execute the question. + click.echo(f"Asking {provider} ({resolved_model})...", err=True) + try: + config = RunConfig( + model_name=resolved_model, + max_tokens=4000, + temperature=0.3, + ) + response = adapter.execute_prompt(question_text, config) + except LLMError as exc: + click.echo(f"LLM error: {exc}", err=True) + sys.exit(1) + + click.echo(response.content) diff --git a/markitect/helper/knowledge.py b/markitect/helper/knowledge.py new file mode 100644 index 00000000..5bc0b563 --- /dev/null +++ b/markitect/helper/knowledge.py @@ -0,0 +1,99 @@ +""" +Knowledge loader for the markitect helper. + +Reads markitect's own documentation files and returns them as a +concatenated string for use as LLM context. +""" + +import importlib +from pathlib import Path +from typing import List + + +# Docs to load relative to the project root, in priority order. +_DOC_PATHS: List[str] = [ + "INTRODUCTION.md", + "docs/CLI_TUTORIAL.md", + "docs/PROJECT_STRUCTURE.md", + "docs/SCHEMA_MANAGEMENT_GUIDE.md", + "docs/PLUGIN_SYSTEM.md", + "docs/ERROR_HANDLING_STRATEGY.md", + "docs/architecture/CAPABILITIES_ARCHITECTURE.md", + "docs/architecture/caching-system.md", + "docs/ASSET_MANAGEMENT_USER_GUIDE.md", + "docs/graphql_interface.md", + "examples/content-generator/TUTORIAL.md", + "examples/infospace-with-history/TUTORIAL.md", +] + +# Glob patterns (relative to project root) for additional docs. +_DOC_GLOBS: List[str] = [ + "docs/user-guides/*.md", +] + +# Modules whose docstrings to include. +_MODULE_DOCSTRINGS: List[str] = [ + "markitect.prompts", + "markitect.llm", +] + + +def _find_project_root() -> Path: + """Return the markitect project root directory. + + Walks up from this file (markitect/helper/knowledge.py) to find the + directory that contains ``markitect/`` as a package *and* has a + ``pyproject.toml`` or ``INTRODUCTION.md``. + """ + candidate = Path(__file__).resolve().parent.parent.parent + # Verify we landed in the right place. + if (candidate / "pyproject.toml").exists() or (candidate / "INTRODUCTION.md").exists(): + return candidate + # Fallback: try CWD. + cwd = Path.cwd() + if (cwd / "markitect").is_dir(): + return cwd + return candidate + + +def collect_knowledge() -> str: + """Load markitect documentation and return as a single string. + + Reads documentation files from the project root, concatenates them + with section headers, and appends relevant module docstrings. + Missing files are silently skipped. + """ + root = _find_project_root() + sections: List[str] = [] + + # Fixed-path documents. + for rel_path in _DOC_PATHS: + filepath = root / rel_path + if filepath.is_file(): + try: + content = filepath.read_text(encoding="utf-8") + sections.append(f"## {rel_path}\n\n{content}") + except OSError: + continue + + # Glob-pattern documents. + for pattern in _DOC_GLOBS: + for filepath in sorted(root.glob(pattern)): + if filepath.is_file(): + rel = filepath.relative_to(root) + try: + content = filepath.read_text(encoding="utf-8") + sections.append(f"## {rel}\n\n{content}") + except OSError: + continue + + # Module docstrings. + for mod_name in _MODULE_DOCSTRINGS: + try: + mod = importlib.import_module(mod_name) + if mod.__doc__: + sections.append(f"## Module: {mod_name}\n\n{mod.__doc__.strip()}") + except ImportError: + continue + + return "\n\n---\n\n".join(sections) diff --git a/roadmap/helper-command/PLAN.md b/roadmap/helper-command/PLAN.md new file mode 100644 index 00000000..ce5f8b1c --- /dev/null +++ b/roadmap/helper-command/PLAN.md @@ -0,0 +1,95 @@ +# Plan: `markitect helper` CLI Command + +## Context + +Add an interactive Q&A facility to markitect that answers questions about markitect itself using its own documentation as knowledge context. Uses the existing LLM adapter infrastructure with OpenRouter and `openrouter/aurora-alpha` as default. + +## New Files + +### 1. `markitect/helper/__init__.py` +Empty package init. + +### 2. `markitect/helper/knowledge.py` — Knowledge Loader + +- `collect_knowledge() -> str` function that: + - Finds markitect's documentation directory relative to the package root (`Path(__file__).resolve().parent.parent.parent`) + - Reads key markdown files in priority order: + 1. `INTRODUCTION.md` + 2. `docs/CLI_TUTORIAL.md` + 3. `docs/PROJECT_STRUCTURE.md` + 4. `docs/SCHEMA_MANAGEMENT_GUIDE.md` + 5. `docs/PLUGIN_SYSTEM.md` + 6. `docs/ERROR_HANDLING_STRATEGY.md` + 7. `docs/architecture/CAPABILITIES_ARCHITECTURE.md` + 8. `docs/architecture/caching-system.md` + 9. `docs/user-guides/*.md` (all files) + 10. `examples/content-generator/TUTORIAL.md` + 11. `examples/infospace-with-history/TUTORIAL.md` + 12. Module docstrings from `markitect/prompts/__init__.py` and `markitect/llm/__init__.py` + - Concatenates with `## ` section headers between each doc + - Skips missing files gracefully (log warning if verbose) + - Returns the combined knowledge string + +### 3. `markitect/helper/cli.py` — Click Command + +```python +@click.command("helper") +@click.argument("question", nargs=-1, required=True) # accepts multi-word questions without quotes +@click.option("--provider", "-p", default="openrouter", + type=click.Choice(["openrouter", "claude-code", "gemini", "openai"]), + help="LLM provider") +@click.option("--model", "-m", default=None, + help="Model name (overrides MARKITECT_HELPER_MODEL env var and default)") +``` + +Logic: +1. Join `question` tuple into a single string +2. Resolve model: CLI `--model` flag → `MARKITECT_HELPER_MODEL` env var → `openrouter/aurora-alpha` +3. Call `collect_knowledge()` to build the knowledge context +4. Build system prompt: `"You are a markitect expert assistant. Answer questions based on the following markitect documentation:\n\n{knowledge}"` +5. Call `create_adapter(provider, model=model, system_prompt=system_prompt)` +6. Call `adapter.execute_prompt(question, RunConfig(max_tokens=4000, temperature=0.3))` +7. Print `response.content` to stdout +8. Handle errors: `LLMConfigurationError` → helpful message about API key, other `LLMError` → stderr message + +## Modified Files + +### 4. `markitect/cli.py` — Register the Command + +At the bottom with the other `cli.add_command()` calls, add: + +```python +try: + from markitect.helper.cli import helper_command + cli.add_command(helper_command) +except ImportError: + pass # Helper module not available +``` + +This follows the existing pattern used for prompts, finance, etc. + +## Configuration + +- **Default provider**: `openrouter` +- **Default model**: `openrouter/aurora-alpha` +- **Environment variable**: `MARKITECT_HELPER_MODEL` — overrides the default model +- **CLI flag**: `--model` / `-m` — overrides both env var and default +- **Precedence**: `--model` flag > `MARKITECT_HELPER_MODEL` env var > `openrouter/aurora-alpha` + +## Key Reference Files + +- `markitect/cli.py` — main CLI (Click-based, `@cli.command()` pattern, register via `cli.add_command()`) +- `markitect/llm/factory.py` — `create_adapter(provider, model, api_key, system_prompt)` +- `markitect/llm/openrouter.py` — OpenRouterAdapter (default) +- `markitect/prompts/execution/models.py` — `RunConfig`, `LLMResponse` +- `markitect/prompts/execution/llm_adapter.py` — `LLMAdapter` base class (method: `execute_prompt(prompt, config) -> LLMResponse`) +- `markitect/llm/exceptions.py` — `LLMError`, `LLMConfigurationError`, `LLMAPIError`, `LLMRateLimitError` +- API key resolution: env var `OPENROUTER_API_KEY` or file `apikey-openrouter.txt` in project root + +## Verification + +1. `markitect helper "What is markitect?"` — should return a knowledge-based answer +2. `markitect helper --model anthropic/claude-sonnet-4 "How do schemas work?"` — uses different model +3. `MARKITECT_HELPER_MODEL=google/gemini-2.5-flash markitect helper "What are templates?"` — env var override +4. `markitect helper --provider claude-code "Explain the plugin system"` — uses Claude CLI +5. `markitect helper --help` — shows usage with all options