feat(helper): add interactive Q&A helper command
Add `markitect helper <QUESTION>` CLI command that answers questions about markitect using its own documentation as LLM context. Uses OpenRouter with openrouter/aurora-alpha by default; model is configurable via --model flag or MARKITECT_HELPER_MODEL env var. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
6
markitect/helper/__init__.py
Normal file
6
markitect/helper/__init__.py
Normal file
@@ -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.
|
||||
"""
|
||||
101
markitect/helper/cli.py
Normal file
101
markitect/helper/cli.py
Normal file
@@ -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)
|
||||
99
markitect/helper/knowledge.py
Normal file
99
markitect/helper/knowledge.py
Normal file
@@ -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)
|
||||
95
roadmap/helper-command/PLAN.md
Normal file
95
roadmap/helper-command/PLAN.md
Normal file
@@ -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 `## <filename>` 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
|
||||
Reference in New Issue
Block a user