Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
92e452d
Remove unnecessary validate files system
Jun 22, 2025
dc2d58d
move config to agent-dcs dir
Jun 22, 2025
e033b8f
remove config from root dir
Jun 22, 2025
7fe0a04
Add prompt classifier
Jun 23, 2025
73ca415
enhance RAG prompt
Jun 25, 2025
077c70c
Add simple search dialog
Jun 26, 2025
b3f39c9
remove prompt type parsing
Jun 27, 2025
6a34d98
properly rendering Markdown and retrieved documents
Jun 27, 2025
557fee0
Facelift for AI search dialog
Jun 27, 2025
4c015b7
Coderabbit suggested cleanups
Jun 28, 2025
4d76926
fix breaking build
Jun 28, 2025
c3b2a41
move config to env variable
Jun 28, 2025
1d04734
small clean up
Jun 28, 2025
a0a2691
abort controller on component dismount
Jun 28, 2025
29e0598
break custom search dialog into multiple modules
Jun 28, 2025
d34bb9b
linter error fix
Jun 29, 2025
e61a3e3
Result format validation
Jun 29, 2025
ef9e5c5
simplify key down handler
Jun 29, 2025
4e21e6b
enhancing the doc navigation with header locator
Jun 29, 2025
2bb0826
update readme and .env.example
Jun 29, 2025
9de517a
update readme
Jun 30, 2025
abe37e9
outside contributor instruction
Jun 30, 2025
802df85
fix focus indicator
Jul 1, 2025
15d00cc
fix path by removing index
Jul 1, 2025
68cd17e
rephrase prompt for better search context
Jul 1, 2025
b77496d
increase rag timeout
Jul 1, 2025
22c34d2
test fix
Jul 2, 2025
ecceff1
Merge branch 'main' into srith/enhance-qa-agent
Jul 2, 2025
a01ac41
Merge branch 'srith/enhance-qa-agent' of https://github.com/agentuity…
Jul 2, 2025
0dd1459
fix linting error for build
Jul 2, 2025
bc384dd
remove debug event
Jul 2, 2025
5269d49
allow copy and paste on code block - fix the text leaking
Jul 2, 2025
e5ac106
use prebuilt component for code block and cli command
Jul 2, 2025
3023e99
coderabbit suggestions
Jul 2, 2025
049f12c
fix build failure
Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion agent-docs/src/agents/doc-qa/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
import type { AgentContext } from '@agentuity/sdk';
import { generateObject } from 'ai';
import { generateObject, generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

import type { PromptType } from './types';
import { PromptClassificationSchema } from './types';


export async function rephraseVaguePrompt(ctx: AgentContext, input: string): Promise<string> {
const systemPrompt = `You are a technical documentation search assistant for developer tools and AI agents. Your job is to CAREFULLY improve unclear queries ONLY when absolutely necessary.

BE EXTREMELY CONSERVATIVE. Most queries should be returned UNCHANGED.

ONLY rephrase if the query contains:
1. OBVIOUS acronyms that need expansion (SDK, API, CLI, UI, KV, HTTP, REST, JSON, XML)
2. Very vague single words like "error", "setup", "install" without context

NEVER change or "correct" these technical terms (return them exactly as written):
- bun, node, deno (JavaScript runtimes)
- react, vue, angular, svelte (frameworks)
- typescript, javascript, python, rust (languages)
- docker, kubernetes, aws, gcp (platforms)
- Any proper nouns or brand names

When rephrasing:
- Keep the original technical terms EXACTLY as written
- Only add minimal context for clarity
- Don't assume what the user meant
- Don't add implementation details

Examples of GOOD rephrasing:
- "SDK setup" → "SDK setup installation configuration"
- "API error" → "API error handling troubleshooting"
- "CLI install" → "CLI installation setup"

Examples of what to LEAVE UNCHANGED:
- "bun example agent" → "bun example agent" (bun is a known runtime)
- "react component" → "react component" (already clear)
- "node server setup" → "node server setup" (already specific enough)
- "typescript agent" → "typescript agent" (clear technical terms)

If in doubt, return the query UNCHANGED. Better to leave it as-is than to misinterpret the user's intent.

Return ONLY the query text, nothing else.`;

try {
const result = await generateText({
model: openai('gpt-4o-mini'),
system: systemPrompt,
prompt: `User query: "${input}"`,
maxTokens: 100,
temperature: 0.1,
});

const rephrasedQuery = result.text?.trim() || input;
console.log(rephrasedQuery);
// Log if we actually rephrased it
if (rephrasedQuery !== input) {
ctx.logger.info('Rephrased query from "%s" to "%s"', input, rephrasedQuery);
}

return rephrasedQuery;

} catch (error) {
ctx.logger.error('Error rephrasing prompt, returning original: %o', error);
return input;
}
}

/**
* Determines the prompt type based on the input string using LLM classification.
* Uses specific, measurable criteria to decide between Normal and Agentic RAG.
Expand Down
29 changes: 21 additions & 8 deletions agent-docs/src/agents/doc-qa/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,32 @@ import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';

import { retrieveRelevantDocs } from './retriever';
import { rephraseVaguePrompt } from './prompt';
import { AnswerSchema } from './types';
import type { Answer } from './types';

export default async function answerQuestion(ctx: AgentContext, prompt: string) {
const relevantDocs = await retrieveRelevantDocs(ctx, prompt);
// First, rephrase the prompt for better vector search
const rephrasedPrompt = await rephraseVaguePrompt(ctx, prompt);

// Use the rephrased prompt for document retrieval
const relevantDocs = await retrieveRelevantDocs(ctx, rephrasedPrompt);

const systemPrompt = `
You are Agentuity's developer-documentation assistant.

=== CONTEXT ===
You will receive both the user's ORIGINAL question and a REPHRASED version that was optimized for document search. The rephrased version helped find the relevant documents, but you should answer the user's original intent.

=== RULES ===
1. Use ONLY the content inside <DOCS> tags to craft your reply. If the required information is missing, state that the docs do not cover it.
2. Never fabricate or guess undocumented details.
3. Ambiguity handling:
3. Focus on answering the ORIGINAL QUESTION, using the documents found via the rephrased search.
4. Ambiguity handling:
• When <DOCS> contains more than one distinct workflow or context that could satisfy the question, do **not** choose for the user.
• Briefly (≤ 2 sentences each) summarise each plausible interpretation and ask **one** clarifying question so the user can pick a path.
• Provide a definitive answer only after the ambiguity is resolved.
4. Answer style:
5. Answer style:
• If the question can be answered unambiguously from a single workflow, give a short, direct answer.
• Add an explanation only when the user explicitly asks for one.
• Format your response in **MDX (Markdown Extended)** format with proper syntax highlighting for code blocks.
Expand All @@ -28,8 +37,8 @@ You are Agentuity's developer-documentation assistant.
• Wrap code snippets in appropriate language blocks (e.g., \`\`\`typescript, \`\`\`json, \`\`\`javascript).
• Use **bold** for important terms and *italic* for emphasis when appropriate.
• Use > blockquotes for important notes or warnings.
5. You may suggest concise follow-up questions or related topics that are present in <DOCS>.
6. Keep a neutral, factual tone.
6. You may suggest concise follow-up questions or related topics that are present in <DOCS>.
7. Keep a neutral, factual tone.

=== OUTPUT FORMAT ===
Return **valid JSON only** matching this TypeScript type:
Expand Down Expand Up @@ -74,9 +83,13 @@ agentuity agent create [name] [description] [auth_type]

> **Note**: This command will create the agent in the Agentuity Cloud and set up local files.

<QUESTION>
<ORIGINAL_QUESTION>
${prompt}
</QUESTION>
</ORIGINAL_QUESTION>

<REPHRASED_SEARCH_QUERY>
${rephrasedPrompt}
</REPHRASED_SEARCH_QUERY>

<DOCS>
${JSON.stringify(relevantDocs, null, 2)}
Expand All @@ -87,7 +100,7 @@ ${JSON.stringify(relevantDocs, null, 2)}
const result = await generateObject({
model: openai('gpt-4o'),
system: systemPrompt,
prompt: prompt,
prompt: `Please answer the original question using the documentation found via the rephrased search query. Your answer should cater toward the original user prompt rather than the rephrased version of the query.`,
schema: AnswerSchema,
maxTokens: 2048,
});
Expand Down
23 changes: 22 additions & 1 deletion app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,28 @@ const { GET: defaultSearchHandler } = createFromSource(source);

function documentPathToUrl(docPath: string): string {
// Remove the .md or .mdx extension before any # symbol
return '/' + docPath.replace(/\.mdx?(?=#|$)/, '');
const path = docPath.replace(/\.mdx?(?=#|$)/, '');

// Split path and hash (if any)
const [basePath, hash] = path.split('#');

// Split the base path into segments
const segments = basePath.split('/').filter(Boolean);

// If the last segment is 'index', remove it
if (segments.length > 0 && segments[segments.length - 1].toLowerCase() === 'index') {
segments.pop();
}

// Reconstruct the path
let url = '/' + segments.join('/');
if (url === '/') {
url = '/';
}
if (hash) {
url += '#' + hash;
}
return url;
}

// Helper function to get document title and description from source
Expand Down
18 changes: 11 additions & 7 deletions bin/send-webhook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,31 @@ RETRY_DELAY=2

echo "Sending webhook to $WEBHOOK_URL" >&2

# Read payload from stdin
payload=$(cat)
# Create temporary file for payload
TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"' EXIT

if [ -z "$payload" ]; then
# Read payload from stdin to temporary file
cat > "$TEMP_FILE"

if [ ! -s "$TEMP_FILE" ]; then
echo "Error: No payload received from stdin" >&2
exit 1
fi

# Validate JSON
if ! echo "$payload" | jq . >/dev/null 2>&1; then
if ! jq . "$TEMP_FILE" >/dev/null 2>&1; then
echo "Error: Invalid JSON payload" >&2
exit 1
fi

echo "Payload size: $(echo "$payload" | wc -c) bytes" >&2
echo "Payload size: $(wc -c < "$TEMP_FILE") bytes" >&2

# Build curl command
# Build curl command using temporary file
curl_args=(
-X POST
-H "Content-Type: application/json"
-d "$payload"
--data-binary "@$TEMP_FILE"
--fail
--show-error
--silent
Expand Down
48 changes: 45 additions & 3 deletions components/CustomSearchDialog/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import { useRef, useEffect } from 'react';
import React, { useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { User, HelpCircle, Loader2 } from 'lucide-react';
import { AgentuityLogo } from '../icons/AgentuityLogo';
import { CLICommand } from '../CLICommand';
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
import { MessageListProps, Message } from './types';

export function MessageList({ messages, loading, handleSourceClick }: MessageListProps) {
Expand Down Expand Up @@ -87,12 +89,52 @@ function MessageItem({ message, handleSourceClick }: MessageItemProps) {
}`}>
{message.type === 'ai' ? (
<div className="prose prose-sm max-w-none dark:prose-invert prose-gray text-sm">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
pre: ({ children }) => {
// Extract code content and language
const codeElement = React.Children.toArray(children)[0] as React.ReactElement<{
className?: string;
children?: React.ReactNode;
}>;
const className = codeElement?.props?.className || '';
const language = className.replace('language-', '');

// Extract string content from children
const code = typeof codeElement?.props?.children === 'string'
? codeElement.props.children
: String(codeElement?.props?.children || '');

// Use CLICommand for bash/shell commands
if (language === 'bash' || language === 'sh' || language === 'shell') {
return <CLICommand command={code} />;
}

return (
<DynamicCodeBlock
code={code}
lang={language || 'text'}
/>
);
},
code: ({ children, className, ...props }) => {
if (!className) {
return (
<code className="break-words whitespace-pre-wrap" {...props}>
{children}
</code>
);
}
return <code {...props}>{children}</code>;
}
}}
>
{message.content}
</ReactMarkdown>
</div>
) : (
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
<p className="text-sm whitespace-pre-wrap break-words">{message.content}</p>
)}
</div>

Expand Down
34 changes: 23 additions & 11 deletions components/CustomSearchDialog/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ export function SearchInput({ currentInput, setCurrentInput, loading, sendMessag
}
};

const handleSend = () => {
if (currentInput.trim()) {
sendMessage(currentInput);
}
};

return (
<div className="w-full">
<div className="flex gap-2">
<div className="flex-1 relative">
{/* Textarea Container */}
<div className="flex-1 min-w-0 relative">
<textarea
ref={textareaRef}
value={currentInput}
Expand All @@ -44,20 +51,25 @@ export function SearchInput({ currentInput, setCurrentInput, loading, sendMessag
disabled={loading}
rows={1}
/>
{currentInput.trim() && (
<div className="absolute right-2 bottom-2 text-xs text-gray-400">
{currentInput.trim() && !loading && (
<div className="absolute right-2 bottom-2 text-xs text-gray-400 pointer-events-none">
Press Enter to send
</div>
)}
</div>
<button
onClick={() => currentInput.trim() && sendMessage(currentInput)}
disabled={loading || !currentInput.trim()}
className="px-3 py-2 bg-gray-800 hover:bg-gray-900 disabled:bg-gray-200 dark:disabled:bg-gray-700 disabled:text-gray-400 text-white text-sm rounded-lg transition-colors disabled:cursor-not-allowed flex-shrink-0"
aria-label="Send message"
>
<Send className="w-4 h-4" />
</button>

{/* Button Container - this is the key fix */}
<div className="flex flex-col">
<button
onClick={handleSend}
disabled={loading || !currentInput.trim()}
className="h-[40px] w-[40px] bg-gray-800 hover:bg-gray-900 disabled:bg-gray-200 dark:disabled:bg-gray-700 disabled:text-gray-400 text-white text-sm rounded-lg transition-colors disabled:cursor-not-allowed flex items-center justify-center flex-shrink-0"
aria-label="Send message"
type="button"
>
<Send className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion components/CustomSearchDialog/hooks/useMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function useMessages() {

const controller = new AbortController();
abortControllerRef.current = controller;
const timeoutId = setTimeout(() => controller.abort(), 30000);
const timeoutId = setTimeout(() => controller.abort(), 90000);

const response = await fetch(`/api/search?${searchParams}`, {
signal: controller.signal
Expand Down Expand Up @@ -142,6 +142,7 @@ export function useMessages() {
}, []);

const handleSourceClick = useCallback((url: string) => {

if (url && url !== '#' && (url.startsWith('/') || url.startsWith('http'))) {
window.open(url, '_blank');
}
Expand Down
Loading