mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-27 16:55:02 +00:00
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:
@@ -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 } }>;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
@@ -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 } }>;
|
||||
@@ -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 } }>;
|
||||
@@ -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,
|
||||
});
|
||||
+5
-2
@@ -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 } }>;
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user