mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
feat: Add Alquimia AI integration (#2734)
* Add alquimia-ai as new integration: https://alquimia.ai * Fix test cases for alquimia-ai integration. Add alquimia-ai to workflow.yml * Add install url to alquimia-ai integration * Renamed alquimia-ai to alquimia (cli native denomination) * Fix unit tests for alquimia integration * Minor fix in alquimia integration * Fix typos and copilot findings * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update tests cases and lint formatting * Final fixes * Rename alquimia_ai to alquimia module integration * Make cli optional for alquimiia integration * resolve review comments * Fix copilot review * Minor fixes: naming, remove unused code * Update tests cases. Fix issues * Fix unit tests * Add alquimia context to default agent-context extension. Update cli requirment to support workflows * Fix hints (suggestion) * Add Alquimia AI as agent in github issue template. Fix unit tests * Address review comments. Update docs * Update test cases --------- Co-authored-by: Eric Engstfeld <ericengstfeld@Erics-MacBook-Pro.local> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -48,6 +48,7 @@ def _register_builtins() -> None:
|
||||
"""
|
||||
# -- Imports (alphabetical) -------------------------------------------
|
||||
from .agy import AgyIntegration
|
||||
from .alquimia import AlquimiaAIIntegration
|
||||
from .amp import AmpIntegration
|
||||
from .auggie import AuggieIntegration
|
||||
from .bob import BobIntegration
|
||||
@@ -86,6 +87,7 @@ def _register_builtins() -> None:
|
||||
|
||||
# -- Registration (alphabetical) --------------------------------------
|
||||
_register(AgyIntegration())
|
||||
_register(AlquimiaAIIntegration())
|
||||
_register(AmpIntegration())
|
||||
_register(AuggieIntegration())
|
||||
_register(BobIntegration())
|
||||
|
||||
165
src/specify_cli/integrations/alquimia/__init__.py
Normal file
165
src/specify_cli/integrations/alquimia/__init__.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Alquimia AI integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..._utils import dump_frontmatter
|
||||
from ..base import SkillsIntegration
|
||||
|
||||
# Mapping of command template stem → argument-hint text shown inline
|
||||
# when a user invokes the slash command in Alquimia AI.
|
||||
ARGUMENT_HINTS: dict[str, str] = {
|
||||
"specify": "Describe the feature you want to specify",
|
||||
"plan": "Optional guidance for the planning phase",
|
||||
"tasks": "Optional task generation constraints",
|
||||
"implement": "Optional implementation guidance or task filter",
|
||||
"analyze": "Optional focus areas for analysis",
|
||||
"clarify": "Optional areas to clarify in the spec",
|
||||
"constitution": "Principles or values for the project constitution",
|
||||
"checklist": "Domain or focus area for the checklist",
|
||||
"taskstoissues": "Optional filter or label for GitHub issues",
|
||||
}
|
||||
|
||||
|
||||
class AlquimiaAIIntegration(SkillsIntegration):
|
||||
"""Integration for Alquimia AI skills."""
|
||||
|
||||
key = "alquimia"
|
||||
config = {
|
||||
"name": "Alquimia AI",
|
||||
"folder": ".alquimia/",
|
||||
"commands_subdir": "skills",
|
||||
"install_url": "https://docs.alquimia.ai",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".alquimia/skills",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
multi_install_safe = True
|
||||
|
||||
def _render_skill(
|
||||
self, template_name: str, frontmatter: dict[str, Any], body: str
|
||||
) -> str:
|
||||
"""Render a processed command template as an Alquimia skill."""
|
||||
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
||||
description = frontmatter.get(
|
||||
"description",
|
||||
f"Spec-kit workflow command: {template_name}",
|
||||
)
|
||||
skill_frontmatter = self._build_skill_fm(
|
||||
skill_name, description, f"templates/commands/{template_name}.md"
|
||||
)
|
||||
frontmatter_text = dump_frontmatter(skill_frontmatter)
|
||||
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
|
||||
|
||||
def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
|
||||
return CommandRegistrar.build_skill_frontmatter(
|
||||
self.key, name, description, source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def inject_argument_hint(content: str, hint: str) -> str:
|
||||
"""Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.
|
||||
|
||||
Skips injection if ``argument-hint:`` already exists in the
|
||||
frontmatter to avoid duplicate keys.
|
||||
"""
|
||||
lines = content.splitlines(keepends=True)
|
||||
|
||||
# Pre-scan: bail out if argument-hint already present in frontmatter
|
||||
dash_count = 0
|
||||
for line in lines:
|
||||
stripped = line.rstrip("\n\r")
|
||||
if stripped == "---":
|
||||
dash_count += 1
|
||||
if dash_count == 2:
|
||||
break
|
||||
continue
|
||||
if dash_count == 1 and stripped.startswith("argument-hint:"):
|
||||
return content # already present
|
||||
|
||||
out: list[str] = []
|
||||
in_fm = False
|
||||
dash_count = 0
|
||||
injected = False
|
||||
for line in lines:
|
||||
stripped = line.rstrip("\n\r")
|
||||
if stripped == "---":
|
||||
dash_count += 1
|
||||
in_fm = dash_count == 1
|
||||
out.append(line)
|
||||
continue
|
||||
if in_fm and not injected and stripped.startswith("description:"):
|
||||
out.append(line)
|
||||
# Preserve the exact line-ending style (\r\n vs \n)
|
||||
if line.endswith("\r\n"):
|
||||
eol = "\r\n"
|
||||
elif line.endswith("\n"):
|
||||
eol = "\n"
|
||||
else:
|
||||
eol = ""
|
||||
escaped = hint.replace("\\", "\\\\").replace('"', '\\"')
|
||||
out.append(f'argument-hint: "{escaped}"{eol}')
|
||||
injected = True
|
||||
continue
|
||||
out.append(line)
|
||||
return "".join(out)
|
||||
|
||||
@staticmethod
|
||||
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
|
||||
"""Insert ``key: value`` before the closing ``---`` if not already present."""
|
||||
lines = content.splitlines(keepends=True)
|
||||
|
||||
# Pre-scan: bail out if already present in frontmatter
|
||||
dash_count = 0
|
||||
for line in lines:
|
||||
stripped = line.rstrip("\n\r")
|
||||
if stripped == "---":
|
||||
dash_count += 1
|
||||
if dash_count == 2:
|
||||
break
|
||||
continue
|
||||
if dash_count == 1 and stripped.startswith(f"{key}:"):
|
||||
return content
|
||||
|
||||
# Inject before the closing --- of frontmatter
|
||||
out: list[str] = []
|
||||
dash_count = 0
|
||||
injected = False
|
||||
for line in lines:
|
||||
stripped = line.rstrip("\n\r")
|
||||
if stripped == "---":
|
||||
dash_count += 1
|
||||
if dash_count == 2 and not injected:
|
||||
if line.endswith("\r\n"):
|
||||
eol = "\r\n"
|
||||
elif line.endswith("\n"):
|
||||
eol = "\n"
|
||||
else:
|
||||
eol = ""
|
||||
out.append(f"{key}: {value}{eol}")
|
||||
injected = True
|
||||
out.append(line)
|
||||
return "".join(out)
|
||||
|
||||
def post_process_skill_content(self, content: str) -> str:
|
||||
"""Inject Alquimia-specific frontmatter flags, hints and hook notes."""
|
||||
updated = super().post_process_skill_content(content)
|
||||
updated = self._inject_frontmatter_flag(updated, "user-invocable")
|
||||
updated = self._inject_frontmatter_flag(
|
||||
updated, "disable-model-invocation", "false"
|
||||
)
|
||||
for line in updated.splitlines():
|
||||
if line.startswith("name:"):
|
||||
name = line.removeprefix("name:").strip().strip("\"'")
|
||||
hint = ARGUMENT_HINTS.get(name.removeprefix("speckit-"))
|
||||
if hint:
|
||||
updated = self.inject_argument_hint(updated, hint)
|
||||
break
|
||||
return updated
|
||||
Reference in New Issue
Block a user