-
Notifications
You must be signed in to change notification settings - Fork 97
LCORE-1426: refactor RAG configuration into unified 'rag' section [Firefly test] #2298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -397,3 +397,75 @@ Allowed prefixes: `LCORE-`, `RSPEED-`, `MGTM-`, `OLS-`, `RHIDP-`, `LEADS-`, `CWF | |
| 4. Follow existing code patterns in the module you're modifying | ||
| 5. Write unit tests covering new functionality | ||
| 6. Run format and verify before completion | ||
|
|
||
| ## Lola Skills | ||
|
|
||
| These skills are installed by Lola and provide specialized capabilities. | ||
| When a task matches a skill's description, read the skill's SKILL.md file | ||
| to learn the detailed instructions and workflows. | ||
|
|
||
| **How to use skills:** | ||
| 1. Check if your task matches any skill description below | ||
| 2. Use `read_file` to read the skill's SKILL.md for detailed instructions | ||
| 3. Follow the instructions in the SKILL.md file | ||
|
|
||
| <!-- lola:skills:start --> | ||
|
|
||
| ### ixd-firefly-custom-skills | ||
|
|
||
| #### api-spec-validator | ||
| **When to use:** Validate API endpoint implementations against OpenAPI or Swagger specifications for correctness and completeness. Use when checking API compliance, auditing spec drift, or verifying that route handlers match their documented contracts. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/api-spec-validator/SKILL.md` for detailed guidance. | ||
|
|
||
| #### code-reviewer | ||
| **When to use:** Perform structured code reviews with a comprehensive checklist covering correctness, security, performance, and maintainability. Use when reviewing a diff, pull request, merge request, or set of changed files before merge. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/code-reviewer/SKILL.md` for detailed guidance. | ||
|
|
||
| #### coverage-analyzer | ||
| **When to use:** Analyze test coverage reports to identify gaps in line, branch, and function coverage, then suggest targeted tests to close those gaps and reach 100% coverage. Use when coverage is below target, after adding new code, or when preparing for a release. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/coverage-analyzer/SKILL.md` for detailed guidance. | ||
|
|
||
| #### cypress-skill | ||
| **When to use:** Generates production-grade Cypress E2E and component tests in JavaScript or TypeScript, with support for local execution. Use when the user asks to write or debug Cypress tests, set up Cypress, work with cy commands, or mentions Cypress, "component test", "E2E test", "cy.", or common APIs such as cy.visit, cy.get, or cy.intercept. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/cypress-skill/SKILL.md` for detailed guidance. | ||
|
|
||
| #### evolution | ||
| **When to use:** Updates skill files based on user corrections during a session. Use when a user's guidance contradicts or extends a loaded skill's instructions, enabling skills to learn and improve from real-world usage. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/evolution/SKILL.md` for detailed guidance. | ||
|
|
||
| #### flaky-test-detector | ||
| **When to use:** Identify flaky tests by running test suites multiple times and analyzing inconsistent pass/fail results. Use when tests fail intermittently, CI builds are unreliable, or when diagnosing non-deterministic test behavior. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/flaky-test-detector/SKILL.md` for detailed guidance. | ||
|
|
||
| #### security-scanner | ||
| **When to use:** Scan source code for common security vulnerabilities including injection flaws, authentication issues, sensitive data exposure, XSS, and dependency CVEs. Use when performing security audits, reviewing code before deployment, or hardening an application. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/security-scanner/SKILL.md` for detailed guidance. | ||
|
|
||
| #### test-runner | ||
| **When to use:** Execute project test suites for unit testing with coverage and functional testing. Use when running tests, checking coverage, verifying test results, or executing a specific test file or method. | ||
| **Instructions:** Read `.lola/modules/ixd-firefly-custom-skills/skills/test-runner/SKILL.md` for detailed guidance. | ||
|
|
||
| <!-- lola:skills:end --> | ||
|
|
||
| <!-- lola:instructions:start --> | ||
| <!-- lola:module:ixd-firefly-custom-skills:start --> | ||
| --- | ||
| name: ixd-firefly-custom-skills | ||
| description: Shared agent skills for harness-cli and OpenCode following the Agent Skills standard | ||
| --- | ||
|
|
||
| # Harness Skills Module | ||
|
|
||
| This AI Context Module provides shared skills for use with [harness-cli](https://github.com/your-org/ixd-firefly-harness-engineering-cli) and [OpenCode](https://opencode.ai). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Replace the placeholder repository URL. The link contains 🤖 Prompt for AI Agents |
||
|
|
||
| All skills in this module follow the [Agent Skills](https://agentskills.io) open standard, making them compatible with any agent that supports the specification, including **OpenCode**, **Claude Code**, **Cursor**, **GitHub Copilot**, **Gemini CLI**, and [many others](https://agentskills.io/home). | ||
|
|
||
| ## Available Skills | ||
|
|
||
| Skills include: `test-runner`, `coverage-analyzer`, `flaky-test-detector`, `code-reviewer`, `security-scanner`, `api-spec-validator`, `cypress-skill`, and `evolution` (enables skills to learn from user corrections during sessions). See [SKILLS_QUICKSTART.md](./SKILLS_QUICKSTART.md) for the complete list with descriptions. | ||
|
|
||
| ## Installation | ||
|
|
||
| Install skills into your project using `harness-cli` or manually by copying skill directories into your project's `.opencode/skills/` directory. See [README.md](./README.md) for full installation and usage instructions. | ||
| <!-- lola:module:ixd-firefly-custom-skills:end --> | ||
| <!-- lola:instructions:end --> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |||||||||||||||||||||||||||||||||
| AuthenticationConfiguration, | ||||||||||||||||||||||||||||||||||
| AuthorizationConfiguration, | ||||||||||||||||||||||||||||||||||
| AzureEntraIdConfiguration, | ||||||||||||||||||||||||||||||||||
| ByokRag, | ||||||||||||||||||||||||||||||||||
| CompactionConfiguration, | ||||||||||||||||||||||||||||||||||
| Configuration, | ||||||||||||||||||||||||||||||||||
| ConversationHistoryConfiguration, | ||||||||||||||||||||||||||||||||||
|
|
@@ -534,10 +535,10 @@ def approvals_configuration(self) -> ApprovalsConfiguration: | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||
| def okp(self) -> "OkpConfiguration": | ||||||||||||||||||||||||||||||||||
| """Return OKP configuration.""" | ||||||||||||||||||||||||||||||||||
| """Return OKP configuration from the unified rag section.""" | ||||||||||||||||||||||||||||||||||
| if self._configuration is None: | ||||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| return self._configuration.okp | ||||||||||||||||||||||||||||||||||
| return self._configuration.rag.okp | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+538
to
+541
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Add a The property raises 📝 Proposed change- """Return OKP configuration from the unified rag section."""
+ """Return OKP configuration from the unified rag section.
+
+ Returns:
+ OkpConfiguration: The OKP configuration from ``rag.okp``.
+
+ Raises:
+ LogicError: If the configuration has not been loaded.
+ """As per coding guidelines: "Follow Google Python docstring conventions for all modules, classes, and functions, including relevant Parameters, Returns, Raises, and Attributes sections." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||
| def reranker(self) -> "RerankerConfiguration": | ||||||||||||||||||||||||||||||||||
|
|
@@ -553,6 +554,22 @@ def skills(self) -> Optional[SkillsConfiguration]: | |||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| return self._configuration.skills | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||
| def byok_rag(self) -> list[ByokRag]: | ||||||||||||||||||||||||||||||||||
| """Return BYOK RAG stores from the unified rag configuration. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Backward-compatible property that returns ``rag.byok.stores``. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||
| list[ByokRag]: The list of BYOK RAG store configurations. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||
| LogicError: If the configuration has not been loaded. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| if self._configuration is None: | ||||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| return self._configuration.rag.byok.stores | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||
| def rag_id_mapping(self) -> dict[str, str]: | ||||||||||||||||||||||||||||||||||
| """Return mapping from vector_db_id to rag_id from BYOK and OKP RAG config. | ||||||||||||||||||||||||||||||||||
|
|
@@ -567,12 +584,15 @@ def rag_id_mapping(self) -> dict[str, str]: | |||||||||||||||||||||||||||||||||
| if self._configuration is None: | ||||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| byok_mapping = { | ||||||||||||||||||||||||||||||||||
| brag.vector_db_id: brag.rag_id for brag in self._configuration.byok_rag | ||||||||||||||||||||||||||||||||||
| brag.vector_db_id: brag.rag_id | ||||||||||||||||||||||||||||||||||
| for brag in self._configuration.rag.byok.stores | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| rag = self._configuration.rag | ||||||||||||||||||||||||||||||||||
| okp_id = constants.OKP_RAG_ID | ||||||||||||||||||||||||||||||||||
| okp_enabled = okp_id in (rag.inline or []) or okp_id in (rag.tool or []) | ||||||||||||||||||||||||||||||||||
| inline_sources = rag.retrieval.inline.sources | ||||||||||||||||||||||||||||||||||
| tool_sources = rag.retrieval.tool.sources | ||||||||||||||||||||||||||||||||||
| okp_enabled = okp_id in inline_sources or okp_id in tool_sources | ||||||||||||||||||||||||||||||||||
| okp_mapping = ( | ||||||||||||||||||||||||||||||||||
| {constants.SOLR_DEFAULT_VECTOR_STORE_ID: okp_id} if okp_enabled else {} | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
@@ -593,22 +613,23 @@ def score_multiplier_mapping(self) -> dict[str, float]: | |||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| brag.vector_db_id: brag.score_multiplier | ||||||||||||||||||||||||||||||||||
| for brag in self._configuration.byok_rag | ||||||||||||||||||||||||||||||||||
| for brag in self._configuration.rag.byok.stores | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||
| def inline_solr_enabled(self) -> bool: | ||||||||||||||||||||||||||||||||||
| """Return whether OKP is included in the inline RAG list. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||
| bool: True if 'okp' appears in rag.inline, False otherwise. | ||||||||||||||||||||||||||||||||||
| bool: True if 'okp' appears in rag.retrieval.inline.sources, | ||||||||||||||||||||||||||||||||||
| False otherwise. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||
| LogicError: If the configuration has not been loaded. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| if self._configuration is None: | ||||||||||||||||||||||||||||||||||
| raise LogicError("logic error: configuration is not loaded") | ||||||||||||||||||||||||||||||||||
| return constants.OKP_RAG_ID in self._configuration.rag.inline | ||||||||||||||||||||||||||||||||||
| return constants.OKP_RAG_ID in self._configuration.rag.retrieval.inline.sources | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def resolve_index_name( | ||||||||||||||||||||||||||||||||||
| self, vector_store_id: str, rag_id_mapping: Optional[dict[str, str]] = None | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -469,6 +469,80 @@ def construct_vector_io_providers_section( | |
| return output | ||
|
|
||
|
|
||
| def _extract_byok_rag_list(config: dict[str, Any]) -> list[dict[str, Any]]: | ||
| """Extract BYOK RAG store list from either new or old config format. | ||
|
|
||
| Supports both the new ``rag.byok.stores`` path and the deprecated | ||
| top-level ``byok_rag`` key. | ||
|
|
||
| Parameters: | ||
| config: Raw lightspeed-stack.yaml dict. | ||
|
|
||
| Returns: | ||
| List of BYOK RAG store dicts (may be empty). | ||
| """ | ||
| # New format: rag.byok.stores | ||
| rag = config.get("rag") | ||
| if isinstance(rag, dict): | ||
| byok = rag.get("byok") | ||
| if isinstance(byok, dict): | ||
| stores = byok.get("stores") | ||
| if isinstance(stores, list) and stores: | ||
| return stores | ||
| # Old format: top-level byok_rag | ||
| return config.get("byok_rag") or [] | ||
|
|
||
|
|
||
| def _extract_okp_dict(config: dict[str, Any]) -> dict[str, Any]: | ||
| """Extract OKP configuration from either new or old config format. | ||
|
|
||
| Supports both the new ``rag.okp`` path and the deprecated top-level | ||
| ``okp`` key. | ||
|
|
||
| Parameters: | ||
| config: Raw lightspeed-stack.yaml dict. | ||
|
|
||
| Returns: | ||
| OKP configuration dict (may be empty). | ||
| """ | ||
| # New format: rag.okp | ||
| rag = config.get("rag") | ||
| if isinstance(rag, dict): | ||
| okp = rag.get("okp") | ||
| if isinstance(okp, dict) and okp: | ||
| return okp | ||
| # Old format: top-level okp | ||
| return config.get("okp") or {} | ||
|
|
||
|
|
||
| def _extract_rag_source_ids( | ||
| rag_config: dict[str, Any], | ||
| ) -> tuple[list[str], list[str]]: | ||
| """Extract inline and tool RAG source IDs from either config format. | ||
|
|
||
| Supports both the new ``retrieval.inline.sources`` / ``retrieval.tool.sources`` | ||
| path and the deprecated ``inline`` / ``tool`` keys. | ||
|
|
||
| Parameters: | ||
| rag_config: RAG configuration dict. | ||
|
|
||
| Returns: | ||
| Tuple of (inline_ids, tool_ids). | ||
| """ | ||
| retrieval = rag_config.get("retrieval") | ||
| if isinstance(retrieval, dict): | ||
| inline_cfg = retrieval.get("inline") or {} | ||
| tool_cfg = retrieval.get("tool") or {} | ||
| inline_ids = ( | ||
| inline_cfg.get("sources") or [] if isinstance(inline_cfg, dict) else [] | ||
| ) | ||
| tool_ids = tool_cfg.get("sources") or [] if isinstance(tool_cfg, dict) else [] | ||
| else: | ||
| inline_ids = rag_config.get("inline") or [] | ||
| tool_ids = rag_config.get("tool") or [] | ||
| return inline_ids, tool_ids | ||
|
Comment on lines
+518
to
+543
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Inspect the deprecated rag.inline / rag.tool migration in the Configuration models.
set -euo pipefail
fd -t f 'config.py' src/models --exec rg -n -C15 'inline|tool' -g '*.py' --max-count 200 \
| rg -n -C0 'deprecat|migrate|model_validator|warnings.warn' || true
fd -t f 'config.py' src/models --exec ast-grep run --lang python \
--pattern 'def $NAME(self) -> Self:
$$$'Repository: lightspeed-core/lightspeed-stack Length of output: 50388 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^src/llama_stack_configuration\.py$|src/models/config\.py$|src/.*)' | sed -n '1,120p'
echo "== target function locations =="
rg -n "_extract_rag_source_ids|generate_configuration|synthesize_configuration|migrate_deprecated_rag_fields|generate.*yaml|run\.yaml" src -g '*.py'
echo "== target function =="
sed -n '480,570p' src/llama_stack_configuration.py
echo "== nearby references =="
sed -n '1080,1180p' src/llama_stack_configuration.py
sed -n '1180,1260p' src/llama_stack_configuration.py
sed -n '1320,1400p' src/llama_stack_configuration.pyRepository: lightspeed-core/lightspeed-stack Length of output: 22212 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== target function locations =="
rg -n "_extract_rag_source_ids|generate_configuration|synthesize_configuration|migrate_deprecated_rag_fields|generate.*yaml|run\.yaml" src -g '*.py'
echo "== target function =="
sed -n '480,570p' src/llama_stack_configuration.py
echo "== helpers around run.yaml generation/use =="
sed -n '1080,1180p' src/llama_stack_configuration.py
sed -n '1180,1260p' src/llama_stack_configuration.py
sed -n '1320,1400p' src/llama_stack_configuration.py
echo "== model migration snippet =="
sed -n '2800,2835p' src/models/config.py
sed -n '3255,3295p' src/models/config.pyRepository: lightspeed-core/lightspeed-stack Length of output: 21521 Merge both RAG source-id formats in When raw 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) -> None: | ||
| """Enrich Llama Stack config with BYOK RAG settings. | ||
|
|
||
|
|
@@ -783,8 +857,7 @@ def enrich_solr( # pylint: disable=too-many-locals | |
| - chunk_filter_query (str): Solr filter query for chunk retrieval | ||
| - rhokp_url (str): OKP/Solr base URL (e.g. from ${env.RH_SERVER_OKP}) | ||
| """ | ||
| inline_ids = rag_config.get("inline") or [] | ||
| tool_ids = rag_config.get("tool") or [] | ||
| inline_ids, tool_ids = _extract_rag_source_ids(rag_config) | ||
| okp_enabled = constants.OKP_RAG_ID in inline_ids or constants.OKP_RAG_ID in tool_ids | ||
|
|
||
| if not okp_enabled: | ||
|
|
@@ -1157,9 +1230,12 @@ def synthesize_configuration( | |
| # 4. Existing enrichment — same calls as legacy generate_configuration so | ||
| # unified output matches legacy output for equivalent inputs (R7). | ||
| enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) | ||
| enrich_byok_rag(ls_config, lcs_config.get("byok_rag", [])) | ||
| byok_rag_list = _extract_byok_rag_list(lcs_config) | ||
| enrich_byok_rag(ls_config, byok_rag_list) | ||
| enrich_vector_store(ls_config, lcs_config.get("vector_store")) | ||
| enrich_solr(ls_config, lcs_config.get("rag", {}), lcs_config.get("okp", {})) | ||
| rag_dict = lcs_config.get("rag", {}) | ||
| okp_dict = _extract_okp_dict(lcs_config) | ||
| enrich_solr(ls_config, rag_dict, okp_dict) | ||
|
|
||
| # 5. High-level inference providers (Decision S5 — a root-level section). | ||
| inference = lcs_config.get("inference") or {} | ||
|
|
@@ -1328,10 +1404,13 @@ def generate_configuration( | |
| enrich_azure_entra_id_inference(ls_config, config.get("azure_entra_id")) | ||
|
|
||
| # Enrichment: BYOK RAG | ||
| enrich_byok_rag(ls_config, config.get("byok_rag", [])) | ||
| byok_rag_list = _extract_byok_rag_list(config) | ||
| enrich_byok_rag(ls_config, byok_rag_list) | ||
|
|
||
| # Enrichment: Solr - enabled when "okp" appears in either inline or tool list | ||
| enrich_solr(ls_config, config.get("rag", {}), config.get("okp", {})) | ||
| rag_dict = config.get("rag", {}) | ||
| okp_dict = _extract_okp_dict(config) | ||
| enrich_solr(ls_config, rag_dict, okp_dict) | ||
|
|
||
| dedupe_providers_vector_io(ls_config) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add blank lines after each skill heading.
markdownlintreports MD022 because each####heading is followed immediately by text. Insert one blank line after every skill heading.Example
#### api-spec-validator + **When to use:** Validate API endpoint implementations...Also applies to: 420-420, 424-424, 428-428, 432-432, 436-436, 440-440, 444-444
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 416-416: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Source: Linters/SAST tools