Skip to content

feat(llm): support configurable output language - #425

Merged
rng1995 merged 3 commits into
NVIDIA:mainfrom
deepujain:feat/345-output-language
Aug 24, 2026
Merged

feat(llm): support configurable output language#425
rng1995 merged 3 commits into
NVIDIA:mainfrom
deepujain:feat/345-output-language

Conversation

@deepujain

Copy link
Copy Markdown
Contributor

Summary

  • Add SKILLSPECTOR_OUTPUT_LANGUAGE for localizing human-readable LLM finding text.
  • Apply the language instruction consistently to discovery analyzers, the meta-analyzer, and TP4's custom prompt path.
  • Preserve rule IDs, severities, categories, file paths, code, and other machine-readable values.
  • Document the new environment variable in .env.example, the README, and the development guide.

Validation

  • .venv/bin/pytest tests/nodes/test_llm_analyzer_base.py tests/test_mcp_tool_poisoning.py -q -m 'not integration and not provider' — 210 passed, 6 deselected; covers unset, blank, trimmed, discovery, meta-analyzer, and TP4 prompt behavior without live provider calls.
  • .venv/bin/ruff check src tests — passed.
  • .venv/bin/ruff format --check src tests — 192 files already formatted.
  • .venv/bin/pytest -m 'not integration and not provider' tests/ -q — 2,804 passed, 13 skipped, 38 deselected, 4 xfailed.
  • git diff --check — passed.

Risk

  • Unset or blank configuration preserves the existing prompts exactly.
  • This controls prompt output language only; it does not translate deterministic static findings or machine-readable schema values.
  • Live provider tests were not run because the behavior is covered at the mocked prompt boundary and requires no provider credentials.

Fixes #345

@yashrajp22
yashrajp22 self-requested a review August 24, 2026 04:31

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SkillSpector Review]

Requesting changes because the new prompt suffix accepts an unbounded, multi-line environment value verbatim. That permits accidental context-window exhaustion and lets configuration text inject instructions after the analyzer's security contract. Please constrain the setting to a short single-line language label (or a documented allowlist/tag format) and add rejection tests for CR/LF and oversized values.

Comment thread src/skillspector/llm_analyzer_base.py Outdated
@deepujain
deepujain force-pushed the feat/345-output-language branch from 8d13fcf to 1d8cda6 Compare August 24, 2026 18:11
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
@deepujain
deepujain force-pushed the feat/345-output-language branch from 1d8cda6 to 5326ce9 Compare August 24, 2026 18:17
@rng1995

rng1995 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks @deepujain for the quick fix and your contribution to SkillSpector. I am approving and merging your PR now!

@rng1995
rng1995 enabled auto-merge (squash) August 24, 2026 18:24
@rng1995
rng1995 merged commit 083522a into NVIDIA:main Aug 24, 2026
5 checks passed
return (
f"{prompt}\n\n## Output language\n\n"
"Write human-readable finding text (including message, finding, explanation, "
f"remediation, and intent fields when present) in {language}. Keep rule IDs, "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line asks the model to write intent in the target language. But the only place intent comes from an LLM is MetaAnalyzerFinding (meta_analyzer.py:101), where it must be exactly "malicious", "negligent", or "benign" — Pydantic rejects anything else. So if the model actually follows this instruction and writes, say, "悪意のある", validation fails, the call retries, and after max attempts the whole meta-analyzer batch fails. OpenAI's structured output would block this server-side, but claude_cli (JSON parsed from plain text) and Ollama via OPENAI_BASE_URL won't — and those are supported setups. Fix is one word: remove intent from this list (or move it to the "keep unchanged" list). Maybe mention impact there too, since it's also a fixed-choice field.

return prompt
return (
f"{prompt}\n\n## Output language\n\n"
"Write human-readable finding text (including message, finding, explanation, "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small one: finding shouldn't be in this list. In this codebase Finding.finding is the "short matched snippet" (models.py:121) — actual text copied from the scanned file, i.e. evidence, not something to translate. No LLM schema returns a finding field today so nothing breaks, but if one ever does, this line would tell the model to translate evidence. Safer to just drop the word.

def build_prompt(self, batch: Batch, **_kwargs: object) -> str:
"""Use TP4's purpose-built prompt without the generic file wrapper."""
return batch.content
return append_output_language_instruction(batch.content)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the language instruction here only translates what the LLM returns — but TP4's final message is built in Python from an English template (_tp4_finding, around line 1047): f"Description-behavior mismatch: declared purpose is '{declared}' but code also performs: {mismatched_text}.", and remediation a few lines below is a hardcoded English string the LLM never touches. So with SKILLSPECTOR_OUTPUT_LANGUAGE=Japanese the user gets an English sentence with Japanese pieces stuffed inside, plus an always-English remediation. Either translate those wrapper templates too, or note in the docs that TP4 messages stay in English.

analyzer_prompt=self.base_prompt,
file_label=batch.file_label,
numbered_content=numbered,
return append_output_language_instruction(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion, not a blocker: instead of wrapping the prompt in all 3 build_prompt methods, wrap it once where the prompt is actually used — the two prompt = self.build_prompt(batch, **kwargs) lines in the run loops (around lines 826 and 925 after this PR). That covers all current analyzers plus any future subclass automatically. Right now, anyone who writes a new build_prompt override has to remember to add this call, and nothing reminds them — TP4 shows overrides do happen. Only cost is adjusting the tests that call build_prompt directly.

return os.environ.get("SKILLSPECTOR_OUTPUT_LANGUAGE", "").strip() or None


def append_output_language_instruction(prompt: str) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this adds ~60 tokens to every prompt, but get_batches budgets file content using only estimate_tokens(self.base_prompt), so the extra text isn't counted. It's noise in practice (the base wrapper isn't fully counted either) — a short comment here would be enough.

file_label=batch.file_label,
file_content=batch.content,
static_findings=findings_text,
return append_output_language_instruction(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The meta-analyzer prompt ends with "Analyze the findings now:", and this appends the language section after that. Models cope fine, but it reads oddly — a final instruction after the "go" line. Fine to leave (good to fix as well 🙃) ; just flagging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support configurable output language for LLM analyzer findings (e.g. via env var)

3 participants