mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Refactor Hexclave skill context to use documentation instead of skill prompts
- Updated the fetch mechanism to retrieve full documentation from the new URL. - Renamed functions and variables to reflect the change from skill to documentation context. - Adjusted tests to validate the new documentation fetching behavior and error handling. - Removed the obsolete llms-full.txt file as it is no longer needed.
This commit is contained in:
parent
1836e101bb
commit
ebfba5ce2b
@ -1,10 +1,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { _clearSkillCache, getMcpSkillContextPrompt } from "./mcp-skill-context";
|
||||
import { _clearDocsCache, getMcpSkillContextPrompt } from "./mcp-skill-context";
|
||||
|
||||
describe("getMcpSkillContextPrompt", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
_clearSkillCache();
|
||||
_clearDocsCache();
|
||||
});
|
||||
|
||||
it("returns empty string for non-ask_hexclave tool names", async () => {
|
||||
@ -28,41 +28,39 @@ describe("getMcpSkillContextPrompt", () => {
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fetches and embeds the canonical skill for ask_hexclave requests", async () => {
|
||||
it("fetches and embeds the full documentation for ask_hexclave requests", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response("# Hexclave Skill\n\nUse Hexclave docs."),
|
||||
new Response("# Hexclave Docs\n\nUse Hexclave docs."),
|
||||
);
|
||||
|
||||
await expect(getMcpSkillContextPrompt("ask_hexclave")).resolves.toMatchInlineSnapshot(`
|
||||
"
|
||||
|
||||
## MCP-Provided Hexclave Skill Context
|
||||
## MCP-Provided Hexclave Documentation Context
|
||||
|
||||
The current request came through the public Hexclave MCP server's ask_hexclave tool.
|
||||
The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com
|
||||
immediately before spawning this assistant. Treat this skill content as baseline context
|
||||
The backend fetched the full Hexclave documentation from https://docs.hexclave.com/llms-full.txt
|
||||
immediately before spawning this assistant. Treat this documentation as baseline context
|
||||
for answering the user's question, while still using documentation tools for specific
|
||||
facts and citations:
|
||||
|
||||
# Hexclave Skill
|
||||
# Hexclave Docs
|
||||
|
||||
Use Hexclave docs.
|
||||
"
|
||||
`);
|
||||
expect(fetchSpy).toHaveBeenCalledWith("https://skill.hexclave.com", expect.objectContaining({
|
||||
expect(fetchSpy).toHaveBeenCalledWith("https://docs.hexclave.com/llms-full.txt", expect.objectContaining({
|
||||
headers: { Accept: "text/markdown" },
|
||||
signal: expect.any(AbortSignal),
|
||||
}));
|
||||
});
|
||||
|
||||
it("fails loudly when the canonical skill cannot be fetched", async () => {
|
||||
it("fails loudly when the documentation cannot be fetched", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response("missing", { status: 503, statusText: "Service Unavailable" }),
|
||||
);
|
||||
|
||||
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Failed to fetch skill from https://skill.hexclave.com: 503 Service Unavailable]`,
|
||||
);
|
||||
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to fetch docs from https://docs.hexclave.com/llms-full.txt: 503 Service Unavailable]`);
|
||||
});
|
||||
|
||||
it("throws a descriptive error when the fetch times out", async () => {
|
||||
@ -71,14 +69,12 @@ describe("getMcpSkillContextPrompt", () => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Skill fetch from https://skill.hexclave.com timed out after 5000ms]`,
|
||||
);
|
||||
await expect(getMcpSkillContextPrompt("ask_hexclave")).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Docs fetch from https://docs.hexclave.com/llms-full.txt timed out after 5000ms]`);
|
||||
});
|
||||
|
||||
it("returns cached skill on subsequent calls within TTL", async () => {
|
||||
it("returns cached documentation on subsequent calls within TTL", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response("# Cached Skill"),
|
||||
new Response("# Cached Docs"),
|
||||
);
|
||||
|
||||
const first = await getMcpSkillContextPrompt("ask_hexclave");
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
const HEXCLAVE_SKILL_URI = "https://skill.hexclave.com";
|
||||
const HEXCLAVE_DOCS_FULL_URL = "https://docs.hexclave.com/llms-full.txt";
|
||||
const FETCH_TIMEOUT_MS = 5_000;
|
||||
const CACHE_TTL_MS = 5 * 60 * 1_000; // 5 minutes
|
||||
|
||||
let cachedSkill: { text: string, fetchedAt: number } | null = null;
|
||||
let cachedDocs: { text: string, fetchedAt: number } | null = null;
|
||||
|
||||
async function fetchSkillText(): Promise<string> {
|
||||
async function fetchDocsText(): Promise<string> {
|
||||
const now = Date.now();
|
||||
if (cachedSkill && now - cachedSkill.fetchedAt < CACHE_TTL_MS) {
|
||||
return cachedSkill.text;
|
||||
if (cachedDocs && now - cachedDocs.fetchedAt < CACHE_TTL_MS) {
|
||||
return cachedDocs.text;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
@ -15,13 +15,13 @@ async function fetchSkillText(): Promise<string> {
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(HEXCLAVE_SKILL_URI, {
|
||||
response = await fetch(HEXCLAVE_DOCS_FULL_URL, {
|
||||
headers: { Accept: "text/markdown" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
throw new Error(`Skill fetch from ${HEXCLAVE_SKILL_URI} timed out after ${FETCH_TIMEOUT_MS}ms`);
|
||||
throw new Error(`Docs fetch from ${HEXCLAVE_DOCS_FULL_URL} timed out after ${FETCH_TIMEOUT_MS}ms`);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
@ -30,12 +30,12 @@ async function fetchSkillText(): Promise<string> {
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch skill from ${HEXCLAVE_SKILL_URI}: ${response.status} ${response.statusText}`,
|
||||
`Failed to fetch docs from ${HEXCLAVE_DOCS_FULL_URL}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
cachedSkill = { text, fetchedAt: now };
|
||||
cachedDocs = { text, fetchedAt: now };
|
||||
return text;
|
||||
}
|
||||
|
||||
@ -44,24 +44,24 @@ export async function getMcpSkillContextPrompt(toolName: string | null | undefin
|
||||
return "";
|
||||
}
|
||||
|
||||
const skillContext = await fetchSkillText();
|
||||
const docsContext = await fetchDocsText();
|
||||
return `
|
||||
|
||||
## MCP-Provided Hexclave Skill Context
|
||||
## MCP-Provided Hexclave Documentation Context
|
||||
|
||||
The current request came through the public Hexclave MCP server's ask_hexclave tool.
|
||||
The backend fetched the canonical Hexclave agent skill from https://skill.hexclave.com
|
||||
immediately before spawning this assistant. Treat this skill content as baseline context
|
||||
The backend fetched the full Hexclave documentation from https://docs.hexclave.com/llms-full.txt
|
||||
immediately before spawning this assistant. Treat this documentation as baseline context
|
||||
for answering the user's question, while still using documentation tools for specific
|
||||
facts and citations:
|
||||
|
||||
${skillContext}
|
||||
${docsContext}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing only — clears the module-level skill cache.
|
||||
* Exposed for testing only — clears the module-level docs cache.
|
||||
*/
|
||||
export function _clearSkillCache(): void {
|
||||
cachedSkill = null;
|
||||
export function _clearDocsCache(): void {
|
||||
cachedDocs = null;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export type ToolName = typeof TOOL_NAMES[number];
|
||||
export type ToolContext = {
|
||||
auth: SmartRequestAuth | null,
|
||||
targetProjectId?: string | null,
|
||||
mcpToolName?: string | null,
|
||||
};
|
||||
|
||||
export async function getTools(
|
||||
@ -31,6 +32,9 @@ export async function getTools(
|
||||
for (const toolName of toolNames) {
|
||||
switch (toolName) {
|
||||
case "docs": {
|
||||
if (context.mcpToolName === "ask_hexclave") {
|
||||
break;
|
||||
}
|
||||
const docsTools = await createDocsTools();
|
||||
Object.assign(tools, docsTools);
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
|
||||
import { ALL_APPS, AppId } from "@hexclave/shared/dist/apps/apps-config";
|
||||
import { AppIcon, appSquarePaddingExpression, appSquareWidthExpression } from "@hexclave/shared/dist/apps/apps-ui";
|
||||
import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
@ -43,6 +43,7 @@ const APP_ICONS: Record<AppId, React.FunctionComponent<React.SVGProps<SVGSVGElem
|
||||
"tanstack-start": Code,
|
||||
onboarding: ClipboardList,
|
||||
analytics: BarChart3,
|
||||
clickmaps: MousePointerClick,
|
||||
"session-replays": PlayCircle,
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import path from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { llmsTxt } from "../packages/shared/src/ai/llms/llms";
|
||||
import { remindersPrompt } from "../packages/shared/src/ai/unified-prompts/reminders";
|
||||
import { aiSetupPrompt, cliSetupPrompt, convexSetupPrompt, getSdkSetupPrompt, pythonBackendSetupPrompt, restApiBackendSetupPrompt, supabaseSetupPrompt } from "../packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt";
|
||||
import { deindent } from "../packages/shared/src/utils/strings";
|
||||
import { writeFileSyncIfChanged } from "./utils";
|
||||
import { remindersPrompt } from "../packages/shared/src/ai/unified-prompts/reminders";
|
||||
import { buildLlmsFullTxt, llmsTxt } from "../packages/shared/src/ai/llms/llms";
|
||||
|
||||
const generatedComment = "This file is auto-generated by scripts/generate-setup-prompt-docs.ts. Do not edit it manually; edit packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts instead.";
|
||||
type SdkSetupToolCategory = "frontend" | "backend" | "database" | "other";
|
||||
@ -590,8 +590,3 @@ writeFileSyncIfChanged(
|
||||
path.join(repoRoot, "docs-mintlify/llms.txt"),
|
||||
llmsTxt.replace(/[ \t]+$/gm, "") + "\n",
|
||||
);
|
||||
|
||||
writeFileSyncIfChanged(
|
||||
path.join(repoRoot, "docs-mintlify/llms-full.txt"),
|
||||
buildLlmsFullTxt(docsJson).replace(/[ \t]+$/gm, "") + "\n",
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user