rip Inkeep from docs — 100% native Hanzo AI (rag-api + api.hanzo.ai/v1)

docs.hanzo.ai's AI chat now runs entirely on our own stack. The live
/api/chat route was already native (Hanzo gateway) but ungrounded — it
now retrieves from our rag-api index before answering, and cites sources:

  - retrieval: rag-api /query — native index over the docs
  - generation: Hanzo gateway (api.hanzo.ai/v1, zen-coder-flash)
  - sources: provideLinks tool, populated from the indexed metadata

Purged Inkeep repo-wide (only our stack, one way):
  - removed the dead third-party Inkeep handler (lib/inkeep -> lib/ai)
  - widget credit "Powered by Inkeep AI" -> "Powered by Hanzo AI"
  - create-app generator: dead `inkeep` provider -> native `hanzo`
    (installs the ai/hanzo registry bundle; INKEEP_API_KEY -> HANZO_API_KEY)
  - 4 legacy *-docs apps: de-Inkeeped schema, fixed dangling imports

Zero `inkeep` references remain in the codebase.
This commit is contained in:
hanzo-dev
2026-07-23 22:20:25 -07:00
parent b94f115d1b
commit 7ed5e4c7ab
17 changed files with 300 additions and 154 deletions
+5 -5
View File
@@ -16,18 +16,18 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/chat/inkeep-qa-schema';
import type { ProvideLinksToolSchema } from '@/lib/chat/qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { chatEndpoint, publishableKey } from '@/lib/hanzo/client';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/server';
import type { HanzoUIMessage } from '@/lib/chat/qa-schema';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<InkeepUIMessage>;
chat: UseChatHelpers<HanzoUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -261,7 +261,7 @@ const roleName: Record<string, string> = {
assistant: 'hanzo-bot',
};
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: HanzoUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -315,7 +315,7 @@ const systemPrompt =
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat<InkeepUIMessage>({
const chat = useChat<HanzoUIMessage>({
id: 'search',
initialMessages: [{ id: 'system', role: 'system' as const, content: systemPrompt, parts: [] }],
transport: new DefaultChatTransport({
@@ -1,6 +1,7 @@
import type { UIMessage } from 'ai';
import { z } from 'zod';
const InkeepRecordTypes = z.enum([
const HanzoRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
@@ -13,7 +14,7 @@ const InkeepRecordTypes = z.enum([
]);
const LinkType = z.union([
InkeepRecordTypes,
HanzoRecordTypes,
z.string(), // catch all
]);
@@ -48,3 +49,5 @@ const AIAnnotationsToolSchema = z.looseObject({
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
export type HanzoUIMessage = UIMessage<never, { client: { location: string } }>;
+78 -6
View File
@@ -1,10 +1,68 @@
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { streamText } from 'ai';
import { ProvideLinksToolSchema } from '@/lib/ai/qa-schema';
// Hanzo Docs AI — 100% native. Retrieval is our own `rag-api` index over the
// docs; generation is the Hanzo gateway (api.hanzo.ai/v1). No third-party AI —
// everything runs on our own stack. Grounded answers only: the model answers
// from the retrieved context and cites sources via the provideLinks tool.
const SYSTEM_PROMPT =
'You are Hanzo Docs AI, a helpful assistant for the Hanzo AI Cloud documentation. ' +
'Answer questions about Hanzo services, APIs, SDKs, and infrastructure. ' +
'Be concise and accurate.';
'You are Hanzo Docs AI, the assistant for the Hanzo AI Cloud documentation. ' +
'Answer using ONLY the Hanzo documentation context provided below. Cite every ' +
'source you use by calling the provideLinks tool with its URL. Be concise and ' +
'accurate. If the answer is not in the context, say so plainly and point to the ' +
'closest relevant section — never invent APIs, flags, or endpoints.';
// The native RAG index (rag-api), reached through the gateway. `/query` returns
// the top-k indexed doc chunks (page_content + metadata) to ground the answer.
const RAG_URL = process.env.HANZO_RAG_URL ?? 'https://api.hanzo.ai/v1';
const RAG_ENTITY = process.env.HANZO_RAG_ENTITY ?? 'docs.hanzo.ai';
interface RagChunk {
page_content: string;
metadata?: Record<string, unknown>;
}
async function retrieve(apiKey: string, query: string): Promise<string> {
if (!query) return '';
try {
const res = await fetch(`${RAG_URL}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ query, k: 8, entity_id: RAG_ENTITY }),
});
if (!res.ok) return '';
const chunks = (await res.json()) as RagChunk[];
if (!Array.isArray(chunks)) return '';
return chunks
.map(
(c, i) =>
`[${i + 1}] ${(c.metadata?.title as string) ?? ''} ${(c.metadata?.url as string) ?? ''}\n${c.page_content}`,
)
.join('\n\n');
} catch {
return '';
}
}
function lastUserText(
messages: Array<{ role?: string; content?: unknown; parts?: unknown }>,
): string {
const last = [...messages].reverse().find((m) => m.role === 'user');
if (!last) return '';
if (typeof last.content === 'string') return last.content;
const parts = (last.parts ?? last.content) as
| Array<{ type?: string; text?: string }>
| undefined;
if (!Array.isArray(parts)) return '';
return parts
.map((p) => (p.type === 'text' ? p.text ?? '' : ''))
.join(' ')
.trim();
}
export async function POST(req: Request) {
const apiKey = process.env.HANZO_API_KEY ?? process.env.LLM_API_KEY;
@@ -17,15 +75,29 @@ export async function POST(req: Request) {
const { messages } = await req.json();
// Ground the answer on our native index (retrieve for the latest user turn).
const context = await retrieve(apiKey, lastUserText(messages ?? []));
const provider = createOpenAICompatible({
name: 'hanzo',
baseURL: 'https://api.hanzo.ai/v1',
baseURL: process.env.HANZO_AI_BASE_URL ?? 'https://api.hanzo.ai/v1',
apiKey,
});
const result = streamText({
model: provider.chatModel('zen-coder-flash'),
system: SYSTEM_PROMPT,
model: provider.chatModel(process.env.HANZO_AI_MODEL ?? 'zen-coder-flash'),
system:
SYSTEM_PROMPT +
'\n\n' +
(context
? `--- Hanzo docs context ---\n${context}\n--- end context ---`
: '(no matching docs were found in the native index for this query)'),
tools: {
provideLinks: {
inputSchema: ProvideLinksToolSchema,
},
},
toolChoice: 'auto',
messages,
});
+7 -7
View File
@@ -16,17 +16,17 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/inkeep/inkeep-qa-schema';
import type { ProvideLinksToolSchema } from '@/lib/ai/qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { Markdown } from '../markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/route';
import type { HanzoUIMessage } from '@/lib/ai/route';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<InkeepUIMessage>;
chat: UseChatHelpers<HanzoUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -44,8 +44,8 @@ export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div
<p className="text-sm font-medium mb-2">AI Chat</p>
<p className="text-xs text-fd-muted-foreground">
Powered by{' '}
<a href="https://inkeep.com" target="_blank" rel="noreferrer noopener">
Inkeep AI
<a href="https://hanzo.ai" target="_blank" rel="noreferrer noopener">
Hanzo AI
</a>
</p>
</div>
@@ -260,7 +260,7 @@ const roleName: Record<string, string> = {
assistant: 'Hanzo Docs',
};
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: HanzoUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -308,7 +308,7 @@ function Message({ message, ...props }: { message: InkeepUIMessage } & Component
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat<InkeepUIMessage>({
const chat = useChat<HanzoUIMessage>({
id: 'search',
transport: new DefaultChatTransport({
api: '/api/chat',
+4 -4
View File
@@ -195,13 +195,13 @@ export const registry: Registry = {
},
{
type: 'route-handler',
route: 'api/inkeep',
path: 'lib/inkeep/route.ts',
route: 'api/ai',
path: 'lib/ai/route.ts',
},
{
type: 'lib',
path: 'lib/inkeep/inkeep-qa-schema.ts',
target: '<dir>/ai/inkeep-qa-schema.ts',
path: 'lib/ai/qa-schema.ts',
target: '<dir>/ai/qa-schema.ts',
},
],
},
+52
View File
@@ -0,0 +1,52 @@
import { z } from 'zod';
// Native Hanzo docs-AI tool schema (replaces the retired third-party
// schema). The record types are generic source kinds the assistant may cite.
const HanzoRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
'github_issue',
'github_discussion',
'stackoverflow_question',
'discord_forum_post',
'discord_message',
'custom_question_answer',
]);
const LinkType = z.union([
HanzoRecordTypes,
z.string(), // catch all
]);
const LinkSchema = z.looseObject({
label: z.string().nullish(), // the value of the footnote, e.g. `1`
url: z.string(),
title: z.string().nullish(),
type: LinkType.nullish(),
breadcrumbs: z.array(z.string()).nullish(),
});
const LinksSchema = z.array(LinkSchema).nullish();
export const ProvideLinksToolSchema = z.object({
links: LinksSchema,
});
const KnownAnswerConfidence = z.enum([
'very_confident',
'somewhat_confident',
'not_confident',
'no_sources',
'other',
]);
const AnswerConfidence = z.union([KnownAnswerConfidence, z.string()]); // evolvable
const AIAnnotationsToolSchema = z.looseObject({
answerConfidence: AnswerConfidence,
});
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
+111
View File
@@ -0,0 +1,111 @@
import { ProvideLinksToolSchema } from '@/lib/ai/qa-schema';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { $routeHandler } from 'fuma-cli/macros/route-handler';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
export type HanzoUIMessage = UIMessage<
never,
{
client: {
location: string;
};
}
>;
// NATIVE Hanzo AI — replaces the retired third-party hosted RAG. Everything
// runs on our own stack: the `rag-api` native index for retrieval + the Hanzo
// gateway (api.hanzo.ai, OpenAI-compatible) for generation. No third-party AI.
const hanzo = createOpenAICompatible({
name: 'hanzo',
apiKey: process.env.HANZO_API_KEY,
baseURL: process.env.HANZO_AI_BASE_URL ?? 'https://api.hanzo.ai/v1',
});
// The native RAG index (rag-api). `/query` returns the top-k indexed doc chunks
// (page_content + metadata) — we ground the answer on these and cite their URLs.
const RAG_URL = process.env.HANZO_RAG_URL ?? 'https://api.hanzo.ai/v1';
const RAG_ENTITY = process.env.HANZO_RAG_ENTITY ?? 'docs.hanzo.ai';
interface RagChunk {
page_content: string;
metadata?: Record<string, unknown>;
}
async function retrieve(
query: string,
): Promise<{ context: string; links: { url: string; title?: string }[] }> {
try {
const res = await fetch(`${RAG_URL}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.HANZO_API_KEY ?? ''}`,
},
body: JSON.stringify({ query, k: 8, entity_id: RAG_ENTITY }),
});
if (!res.ok) return { context: '', links: [] };
const chunks = (await res.json()) as RagChunk[];
if (!Array.isArray(chunks)) return { context: '', links: [] };
const links = chunks
.map((c) => ({
url: String(c.metadata?.url ?? c.metadata?.source ?? ''),
title: (c.metadata?.title as string | undefined) ?? undefined,
}))
.filter((l) => l.url);
const context = chunks
.map((c, i) => `[${i + 1}] ${(c.metadata?.title as string) ?? ''}\n${c.page_content}`)
.join('\n\n');
return { context, links };
} catch {
return { context: '', links: [] };
}
}
export const handler = $routeHandler(
{
methods: ['POST'],
params: [],
},
async (req) => {
const reqJson = await req.json();
// Ground the answer on the native index: retrieve for the latest user turn.
const msgs = (reqJson.messages ?? []) as HanzoUIMessage[];
const lastUser = [...msgs].reverse().find((m) => m.role === 'user');
const question = (lastUser?.parts ?? [])
.map((p) => ((p as { type?: string; text?: string }).type === 'text' ? (p as { text?: string }).text ?? '' : ''))
.join(' ')
.trim();
const { context } = question ? await retrieve(question) : { context: '' };
const result = streamText({
model: hanzo(process.env.HANZO_AI_MODEL ?? 'zen5'),
system:
'You are the Hanzo documentation assistant. Answer using ONLY the Hanzo ' +
'documentation context below. Cite every source you use by calling the ' +
'provideLinks tool with its URL. If the answer is not in the context, say ' +
'so plainly and point to the closest relevant section.\n\n' +
(context
? `--- Hanzo docs context ---\n${context}\n--- end context ---`
: '(no matching docs were found in the native index for this query)'),
tools: {
provideLinks: {
inputSchema: ProvideLinksToolSchema,
},
},
messages: await convertToModelMessages<HanzoUIMessage>(reqJson.messages, {
ignoreIncompleteToolCalls: true,
convertDataPart(part) {
if (part.type === 'data-client')
return {
type: 'text',
text: `[Client Context: ${JSON.stringify(part.data)}]`,
};
},
}),
toolChoice: 'auto',
});
return result.toUIMessageStreamResponse();
},
);
-51
View File
@@ -1,51 +0,0 @@
import { ProvideLinksToolSchema } from '@/lib/inkeep/inkeep-qa-schema';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { $routeHandler } from 'fuma-cli/macros/route-handler';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
export type InkeepUIMessage = UIMessage<
never,
{
client: {
location: string;
};
}
>;
const openai = createOpenAICompatible({
name: 'inkeep',
apiKey: process.env.INKEEP_API_KEY,
baseURL: 'https://api.inkeep.com/v1',
});
export const handler = $routeHandler(
{
methods: ['POST'],
params: [],
},
async (req) => {
const reqJson = await req.json();
const result = streamText({
model: openai('inkeep-qa-sonnet-4'),
tools: {
provideLinks: {
inputSchema: ProvideLinksToolSchema,
},
},
messages: await convertToModelMessages<InkeepUIMessage>(reqJson.messages, {
ignoreIncompleteToolCalls: true,
convertDataPart(part) {
if (part.type === 'data-client')
return {
type: 'text',
text: `[Client Context: ${JSON.stringify(part.data)}]`,
};
},
}),
toolChoice: 'auto',
});
return result.toUIMessageStreamResponse();
},
);
+5 -5
View File
@@ -16,18 +16,18 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/chat/inkeep-qa-schema';
import type { ProvideLinksToolSchema } from '@/lib/chat/qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { chatEndpoint, publishableKey } from '@/lib/hanzo/client';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/server';
import type { HanzoUIMessage } from '@/lib/chat/qa-schema';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<InkeepUIMessage>;
chat: UseChatHelpers<HanzoUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -261,7 +261,7 @@ const roleName: Record<string, string> = {
assistant: 'hanzo-bot',
};
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: HanzoUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -315,7 +315,7 @@ const systemPrompt =
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat<InkeepUIMessage>({
const chat = useChat<HanzoUIMessage>({
id: 'search',
initialMessages: [{ id: 'system', role: 'system' as const, content: systemPrompt, parts: [] }],
transport: new DefaultChatTransport({
@@ -1,6 +1,7 @@
import type { UIMessage } from 'ai';
import { z } from 'zod';
const InkeepRecordTypes = z.enum([
const HanzoRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
@@ -13,7 +14,7 @@ const InkeepRecordTypes = z.enum([
]);
const LinkType = z.union([
InkeepRecordTypes,
HanzoRecordTypes,
z.string(), // catch all
]);
@@ -48,3 +49,5 @@ const AIAnnotationsToolSchema = z.looseObject({
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
export type HanzoUIMessage = UIMessage<never, { client: { location: string } }>;
+5 -5
View File
@@ -16,18 +16,18 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/chat/inkeep-qa-schema';
import type { ProvideLinksToolSchema } from '@/lib/chat/qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { chatEndpoint, publishableKey } from '@/lib/hanzo/client';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/server';
import type { HanzoUIMessage } from '@/lib/chat/qa-schema';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<InkeepUIMessage>;
chat: UseChatHelpers<HanzoUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -261,7 +261,7 @@ const roleName: Record<string, string> = {
assistant: 'hanzo-bot',
};
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: HanzoUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -315,7 +315,7 @@ const systemPrompt =
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat<InkeepUIMessage>({
const chat = useChat<HanzoUIMessage>({
id: 'search',
initialMessages: [{ id: 'system', role: 'system' as const, content: systemPrompt, parts: [] }],
transport: new DefaultChatTransport({
@@ -1,6 +1,7 @@
import type { UIMessage } from 'ai';
import { z } from 'zod';
const InkeepRecordTypes = z.enum([
const HanzoRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
@@ -13,7 +14,7 @@ const InkeepRecordTypes = z.enum([
]);
const LinkType = z.union([
InkeepRecordTypes,
HanzoRecordTypes,
z.string(), // catch all
]);
@@ -48,3 +49,5 @@ const AIAnnotationsToolSchema = z.looseObject({
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
export type HanzoUIMessage = UIMessage<never, { client: { location: string } }>;
+5 -5
View File
@@ -16,18 +16,18 @@ import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/chat/inkeep-qa-schema';
import type { ProvideLinksToolSchema } from '@/lib/chat/qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { chatEndpoint, publishableKey } from '@/lib/hanzo/client';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/server';
import type { HanzoUIMessage } from '@/lib/chat/qa-schema';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<InkeepUIMessage>;
chat: UseChatHelpers<HanzoUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -261,7 +261,7 @@ const roleName: Record<string, string> = {
assistant: 'hanzo-bot',
};
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: HanzoUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -315,7 +315,7 @@ const systemPrompt =
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat<InkeepUIMessage>({
const chat = useChat<HanzoUIMessage>({
id: 'search',
initialMessages: [{ id: 'system', role: 'system' as const, content: systemPrompt, parts: [] }],
transport: new DefaultChatTransport({
@@ -1,50 +0,0 @@
import { z } from 'zod';
const InkeepRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
'github_issue',
'github_discussion',
'stackoverflow_question',
'discord_forum_post',
'discord_message',
'custom_question_answer',
]);
const LinkType = z.union([
InkeepRecordTypes,
z.string(), // catch all
]);
const LinkSchema = z.looseObject({
label: z.string().nullish(), // the value of the footnote, e.g. `1`
url: z.string(),
title: z.string().nullish(),
type: LinkType.nullish(),
breadcrumbs: z.array(z.string()).nullish(),
});
const LinksSchema = z.array(LinkSchema).nullish();
export const ProvideLinksToolSchema = z.object({
links: LinksSchema,
});
const KnownAnswerConfidence = z.enum([
'very_confident',
'somewhat_confident',
'not_confident',
'no_sources',
'other',
]);
const AnswerConfidence = z.union([KnownAnswerConfidence, z.string()]); // evolvable
const AIAnnotationsToolSchema = z.looseObject({
answerConfidence: AnswerConfidence,
});
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
@@ -1,6 +1,7 @@
import type { UIMessage } from 'ai';
import { z } from 'zod';
const InkeepRecordTypes = z.enum([
const HanzoRecordTypes = z.enum([
'documentation',
'site',
'discourse_post',
@@ -13,7 +14,7 @@ const InkeepRecordTypes = z.enum([
]);
const LinkType = z.union([
InkeepRecordTypes,
HanzoRecordTypes,
z.string(), // catch all
]);
@@ -48,3 +49,5 @@ const AIAnnotationsToolSchema = z.looseObject({
export const ProvideAIAnnotationsToolSchema = z.object({
aiAnnotations: AIAnnotationsToolSchema,
});
export type HanzoUIMessage = UIMessage<never, { client: { location: string } }>;
+5 -5
View File
@@ -39,7 +39,7 @@ const command = program
new Option('--ai-chat <name>', 'configure AI chat').choices([
'openrouter',
'llmgateway',
'inkeep',
'hanzo',
]),
)
.addOption(
@@ -181,7 +181,7 @@ async function main(): Promise<void> {
)
return false;
return select<false | 'openrouter' | 'llmgateway' | 'inkeep'>({
return select<false | 'openrouter' | 'llmgateway' | 'hanzo'>({
message: 'Configure AI Chat?',
options: [
{
@@ -199,9 +199,9 @@ async function main(): Promise<void> {
hint: 'open-source LLM gateway, API key required',
},
{
value: 'inkeep',
label: 'Inkeep AI',
hint: 'API key required',
value: 'hanzo',
label: 'Hanzo AI',
hint: 'native — api.hanzo.ai, API key required',
},
],
});
+3 -3
View File
@@ -7,13 +7,13 @@ import { SyntaxKind } from 'ts-morph';
import { HanzoDocsComponentInstaller } from '@hanzo/docs-cli/registry/installer';
import { HttpRegistryConnector } from 'fuma-cli/registry/connector';
const envKey: Record<'openrouter' | 'llmgateway' | 'inkeep', string> = {
const envKey: Record<'openrouter' | 'llmgateway' | 'hanzo', string> = {
openrouter: 'OPENROUTER_API_KEY',
llmgateway: 'LLM_GATEWAY_API_KEY',
inkeep: 'INKEEP_API_KEY',
hanzo: 'HANZO_API_KEY',
};
export function ai(provider: 'openrouter' | 'llmgateway' | 'inkeep'): TemplatePlugin {
export function ai(provider: 'openrouter' | 'llmgateway' | 'hanzo'): TemplatePlugin {
return {
async afterWrite() {
const config = await getDefaultConfig(this.dest);