refactor(omega): use BM25 auto-loading instead of meta-tools

This commit is contained in:
Ajax Davis 2026-01-23 10:54:02 +10:00
parent 3259e038fb
commit 19a905d9d1
5 changed files with 401 additions and 202 deletions

View file

@ -33,8 +33,6 @@
"@tpmjs/env": "workspace:*",
"@tpmjs/npm-client": "workspace:*",
"@tpmjs/package-executor": "workspace:*",
"@tpmjs/registry-execute": "workspace:*",
"@tpmjs/registry-search": "workspace:*",
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",

View file

@ -4,15 +4,13 @@
* POST: Send a message and stream the AI response via SSE
*
* This endpoint implements the core Omega chat functionality with:
* - Registry search tool for discovering tools
* - Tool executor for running discovered tools
* - Automatic BM25 search to find relevant tools based on user message
* - Dynamic tool loading - top 15 matching tools become available to the AI
* - SSE streaming for real-time updates
*/
import { Prisma, prisma } from '@tpmjs/db';
import { registryExecuteTool } from '@tpmjs/registry-execute';
import { registrySearchTool } from '@tpmjs/registry-search';
import type { ModelMessage } from 'ai';
import { jsonSchema, type ModelMessage } from 'ai';
import { type NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { authenticateRequest } from '~/lib/api-keys/middleware';
@ -40,6 +38,126 @@ const SendMessageSchema = z.object({
message: z.string().min(1).max(10000),
});
// Executor service URL
const EXECUTOR_URL = process.env.TPMJS_EXECUTOR_URL || 'https://executor.tpmjs.com';
/**
* Search for relevant tools based on user query
*/
async function searchRelevantTools(
query: string,
limit = 15
): Promise<
Array<{
toolId: string;
packageName: string;
name: string;
description: string;
version: string;
importUrl: string;
inputSchema?: unknown;
}>
> {
const params = new URLSearchParams({
q: query,
limit: String(limit),
});
// Use internal API (same server)
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000';
const response = await fetch(`${baseUrl}/api/tools/search?${params}`);
if (!response.ok) {
console.error(`Tool search failed: ${response.status} ${response.statusText}`);
return [];
}
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const data = (await response.json()) as any;
const toolsArray = data.results?.tools || [];
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
return toolsArray.map((tool: any) => ({
toolId: `${tool.package.npmPackageName}::${tool.name}`,
packageName: tool.package.npmPackageName,
name: tool.name,
description: tool.description || `Tool: ${tool.name}`,
version: tool.package.npmVersion,
importUrl: `https://esm.sh/${tool.package.npmPackageName}@${tool.package.npmVersion}`,
inputSchema: tool.inputSchema,
}));
}
/**
* Create a dynamic tool wrapper that executes via the sandbox executor
*/
function createDynamicTool(toolMeta: {
toolId: string;
packageName: string;
name: string;
description: string;
version: string;
importUrl: string;
inputSchema?: unknown;
}) {
// Import tool() dynamically to avoid top-level await
const { tool } = require('ai');
return tool({
description: toolMeta.description,
inputSchema: toolMeta.inputSchema
? jsonSchema(toolMeta.inputSchema as Parameters<typeof jsonSchema>[0])
: jsonSchema({
type: 'object',
properties: {},
additionalProperties: true,
}),
// biome-ignore lint/suspicious/noExplicitAny: Dynamic tool params
execute: async (params: any) => {
console.log(`🚀 Executing ${toolMeta.packageName}/${toolMeta.name} with params:`, params);
const response = await fetch(`${EXECUTOR_URL}/execute-tool`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName: toolMeta.packageName,
name: toolMeta.name,
version: toolMeta.version,
importUrl: toolMeta.importUrl,
params,
env: {}, // TODO: Pass user's env vars if needed
}),
});
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const result = (await response.json()) as any;
if (!result.success) {
console.error(`❌ Tool execution failed: ${result.error}`);
throw new Error(result.error || 'Tool execution failed');
}
console.log(`✅ Tool executed in ${result.executionTimeMs}ms`);
return result.output;
},
});
}
/**
* Sanitize tool name to be a valid JS identifier
*/
function sanitizeToolName(name: string): string {
return name
.replace(/@/g, '')
.replace(/\//g, '_')
.replace(/-/g, '_')
.replace(/::/g, '_')
.replace(/[^a-zA-Z0-9_]/g, '');
}
/**
* POST /api/omega/conversations/[id]/messages
* Send a message and stream the AI response via SSE
@ -126,6 +244,27 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
},
});
// 🔍 Search for relevant tools based on user's message
console.log(`🔍 Searching for tools matching: "${parsed.data.message}"`);
const relevantTools = await searchRelevantTools(parsed.data.message, 15);
console.log(`📦 Found ${relevantTools.length} relevant tools`);
// Create dynamic tool wrappers for each found tool
// biome-ignore lint/suspicious/noExplicitAny: Dynamic tool types
const tools: Record<string, any> = {};
for (const toolMeta of relevantTools) {
const sanitizedName = sanitizeToolName(toolMeta.toolId);
try {
tools[sanitizedName] = createDynamicTool(toolMeta);
console.log(`✅ Loaded tool: ${sanitizedName}`);
} catch (error) {
console.error(`❌ Failed to create tool wrapper for ${toolMeta.toolId}:`, error);
}
}
console.log(`🔧 ${Object.keys(tools).length} tools available for this request`);
// Fetch recent messages for context
const recentMessages = await prisma.omegaMessage.findMany({
where: { conversationId },
@ -137,11 +276,35 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
// Build AI SDK messages
const messages: ModelMessage[] = [];
// Add system prompt
const systemPrompt = buildSystemPrompt({
// Build tool list for system prompt
const toolsList = Object.entries(tools)
.map(([name, t]) => {
const tool = t as { description?: string };
return `- ${name}: ${tool.description || 'No description'}`;
})
.join('\n');
// Add system prompt with available tools
const baseSystemPrompt = buildSystemPrompt({
customSystemPrompt: userSettings?.customSystemPrompt,
pinnedToolIds: userSettings?.pinnedToolIds || [],
});
const systemPrompt = `${baseSystemPrompt}
## Available Tools
The following tools have been automatically loaded based on the user's request. Use them directly to accomplish the task:
${toolsList || 'No tools matched this query. Try to help the user with general knowledge.'}
## Instructions
1. If a tool is available that can help, USE IT immediately
2. Don't describe what tools could do - actually call them
3. After calling a tool, explain the results to the user
4. If no tools match, help the user with general knowledge`;
messages.push({ role: 'system', content: systemPrompt });
// Add conversation history
@ -204,12 +367,6 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
// Add new user message
messages.push({ role: 'user', content: parsed.data.message });
// Build Omega tools using published TPMJS packages
const tools = {
registrySearch: registrySearchTool,
registryExecute: registryExecuteTool,
};
// Get the provider model (using OpenAI by default)
const { createOpenAI } = await import('@ai-sdk/openai');
const apiKey = process.env.OPENAI_API_KEY;
@ -253,40 +410,34 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
let inputTokens = 0;
let outputTokens = 0;
// Stream the response with up to 10 tool call iterations
const result = await streamText({
const result = streamText({
model,
messages,
tools,
stopWhen: stepCountIs(10),
onChunk: async ({ chunk }) => {
if (chunk.type === 'tool-call') {
const input = 'args' in chunk ? chunk.args : chunk.input;
console.log('[Omega] Tool call:', {
toolName: chunk.toolName,
toolCallId: chunk.toolCallId,
});
toolCallsMap.set(chunk.toolCallId, {
toolCallId: chunk.toolCallId,
toolName: chunk.toolName,
args: input,
});
tools: Object.keys(tools).length > 0 ? tools : undefined,
stopWhen: stepCountIs(5), // Allow up to 5 tool calls
onChunk: async (chunk) => {
// Handle tool call (complete tool call with args)
if (chunk.chunk.type === 'tool-call') {
const input =
'input' in chunk.chunk
? chunk.chunk.input
: 'args' in chunk.chunk
? (chunk.chunk as { args: unknown }).args
: {};
// Create tool run record
await prisma.omegaToolRun.create({
data: {
conversationId,
toolName: chunk.toolName,
toolName: chunk.chunk.toolName,
input: input as Prisma.InputJsonValue,
status: 'running',
},
});
sendEvent('run.step.tool.started', {
toolCallId: chunk.toolCallId,
toolName: chunk.toolName,
toolCallId: chunk.chunk.toolCallId,
toolName: chunk.chunk.toolName,
input,
});
}
@ -404,37 +555,48 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
});
}
// Update conversation
const updateData: Prisma.OmegaConversationUpdateInput = {
executionState: 'idle',
inputTokensTotal: { increment: inputTokens },
outputTokensTotal: { increment: outputTokens },
updatedAt: new Date(),
};
// Auto-generate title from first message if not set
if (conversation.title === null) {
updateData.title = parsed.data.message.slice(0, 100);
}
// Update conversation token totals
await prisma.omegaConversation.update({
where: { id: conversationId },
data: updateData,
data: {
executionState: 'idle',
inputTokensTotal: { increment: inputTokens },
outputTokensTotal: { increment: outputTokens },
},
});
const executionTimeMs = Date.now() - startTime;
// Update title if this is the first message pair
const messageCount = await prisma.omegaMessage.count({ where: { conversationId } });
if (messageCount <= 3 && !conversation.title) {
// Use first 50 chars of user message as title
const title =
parsed.data.message.slice(0, 50) + (parsed.data.message.length > 50 ? '...' : '');
await prisma.omegaConversation.update({
where: { id: conversationId },
data: { title },
});
}
sendEvent('run.completed', {
messageId: assistantMessage.id,
conversationId,
inputTokens,
outputTokens,
executionTimeMs,
toolCallCount: allToolCalls.length,
toolsLoaded: Object.keys(tools).length,
loadedToolNames: Object.keys(tools),
bm25Results: relevantTools.map((t) => ({
toolId: t.toolId,
name: t.name,
packageName: t.packageName,
description: t.description,
})),
});
} catch (error) {
console.error('[Omega] Stream error:', error);
// Update conversation state
controller.close();
} catch (error) {
console.error('Stream error:', error);
// Update conversation state on error
await prisma.omegaConversation.update({
where: { id: conversationId },
data: { executionState: 'idle' },
@ -443,13 +605,12 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
sendEvent('run.failed', {
error: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
controller.close();
}
},
});
return new NextResponse(stream, {
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
@ -457,7 +618,7 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
},
});
} catch (error) {
console.error('[Omega] Failed to process message:', error);
console.error('Message handler error:', error);
// Reset conversation state
await prisma.omegaConversation.update({
@ -466,10 +627,7 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
});
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process message',
},
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}

View file

@ -7,7 +7,6 @@ import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
import { Streamdown } from 'streamdown';
import { AppHeader } from '~/components/AppHeader';
@ -159,6 +158,7 @@ function ToolCallCard({
/**
* Omega Chat Page
*/
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex chat page with multiple UI states and SSE handling
export default function OmegaChatPage(): React.ReactElement {
const params = useParams();
const router = useRouter();
@ -174,11 +174,17 @@ export default function OmegaChatPage(): React.ReactElement {
const [toolCalls, setToolCalls] = useState<ToolCall[]>([]);
const [expandedToolCalls, setExpandedToolCalls] = useState<Set<string>>(new Set());
const [viewMode, setViewMode] = useState<'chat' | 'debug'>('chat');
const [lastRunInfo, setLastRunInfo] = useState<{
loadedToolNames?: string[];
bm25Results?: Array<{
toolId: string;
name: string;
packageName: string;
description: string;
}>;
} | null>(null);
// Track first item index for prepending (Virtuoso pattern)
const [firstItemIndex, setFirstItemIndex] = useState(10000);
const virtuosoRef = useRef<VirtuosoHandle>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const toggleToolCall = (toolCallId: string) => {
@ -214,7 +220,6 @@ export default function OmegaChatPage(): React.ReactElement {
const { messages: messageList, ...conversationData } = data.data;
setConversation(conversationData);
setMessages(messageList || []);
setFirstItemIndex(10000);
} catch (err) {
console.error('Failed to fetch conversation:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch conversation');
@ -227,6 +232,14 @@ export default function OmegaChatPage(): React.ReactElement {
fetchConversation();
}, [fetchConversation]);
// Auto-scroll to bottom when messages change or streaming
// biome-ignore lint/correctness/useExhaustiveDependencies: We intentionally trigger scroll when messages/streamingContent change
useEffect(() => {
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
}
}, [messages, streamingContent]);
// Check for initial prompt from landing page
// biome-ignore lint/correctness/useExhaustiveDependencies: Only run on mount and when messages load
useEffect(() => {
@ -332,6 +345,11 @@ export default function OmegaChatPage(): React.ReactElement {
);
break;
case 'run.completed':
// Capture tool info for debug view
setLastRunInfo({
loadedToolNames: data.loadedToolNames,
bm25Results: data.bm25Results,
});
// Refresh messages
await fetchConversation();
setStreamingContent('');
@ -477,31 +495,81 @@ export default function OmegaChatPage(): React.ReactElement {
{/* Debug JSON View */}
{viewMode === 'debug' && (
<div className="flex-1 overflow-auto p-4 bg-background">
<div className="max-w-4xl mx-auto">
<div className="mb-4 flex items-start justify-between">
<div className="max-w-4xl mx-auto space-y-6">
{/* BM25 Search Results */}
{lastRunInfo?.bm25Results && lastRunInfo.bm25Results.length > 0 && (
<div>
<h2 className="text-sm font-medium text-foreground mb-2">
Raw Messages Array ({messages.length} messages)
BM25 Search Results ({lastRunInfo.bm25Results.length} tools found)
</h2>
<p className="text-xs text-foreground-tertiary">
Messages are ordered by createdAt. Each message includes role, content, and tool
call data.
<p className="text-xs text-foreground-tertiary mb-3">
Tools discovered from the registry based on the user&apos;s message.
</p>
<div className="bg-surface-secondary border border-border rounded-lg p-4 space-y-2">
{lastRunInfo.bm25Results.map((tool, i) => (
<div key={tool.toolId} className="text-xs font-mono">
<span className="text-foreground-tertiary">{i + 1}.</span>{' '}
<span className="text-primary">{tool.packageName}</span>
<span className="text-foreground-tertiary">::</span>
<span className="text-foreground">{tool.name}</span>
<p className="ml-4 text-foreground-secondary">{tool.description}</p>
</div>
))}
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(messages, null, 2));
}}
>
<Icon icon="copy" size="xs" className="mr-2" />
Copy JSON
</Button>
)}
{/* Loaded Tool Names */}
{lastRunInfo?.loadedToolNames && lastRunInfo.loadedToolNames.length > 0 && (
<div>
<h2 className="text-sm font-medium text-foreground mb-2">
Loaded Tools ({lastRunInfo.loadedToolNames.length} tools)
</h2>
<p className="text-xs text-foreground-tertiary mb-3">
Sanitized tool names available to the AI for this request.
</p>
<div className="bg-surface-secondary border border-border rounded-lg p-4">
<div className="flex flex-wrap gap-2">
{lastRunInfo.loadedToolNames.map((name) => (
<span
key={name}
className="px-2 py-1 bg-primary/10 text-primary text-xs font-mono rounded"
>
{name}
</span>
))}
</div>
</div>
</div>
)}
{/* Messages Array */}
<div>
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-sm font-medium text-foreground mb-2">
Raw Messages Array ({messages.length} messages)
</h2>
<p className="text-xs text-foreground-tertiary">
Messages are ordered by createdAt. Each message includes role, content, and
tool call data.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(messages, null, 2));
}}
>
<Icon icon="copy" size="xs" className="mr-2" />
Copy JSON
</Button>
</div>
<pre className="text-xs font-mono bg-surface-secondary border border-border rounded-lg p-4 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(messages, null, 2)}
</pre>
</div>
<pre className="text-xs font-mono bg-surface-secondary border border-border rounded-lg p-4 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(messages, null, 2)}
</pre>
</div>
</div>
)}
@ -527,103 +595,101 @@ export default function OmegaChatPage(): React.ReactElement {
</div>
</div>
) : (
<Virtuoso
ref={virtuosoRef}
className="h-full"
data={messages}
firstItemIndex={firstItemIndex}
initialTopMostItemIndex={messages.length - 1}
followOutput="smooth"
components={{
Footer: () => (
<div className="px-4 pb-4 space-y-4 max-w-4xl mx-auto">
{/* Live tool calls during streaming */}
{toolCalls.length > 0 && (
<div className="space-y-2">
{toolCalls.map((tc) => (
<div key={tc.toolCallId} className="flex justify-start">
<div className="max-w-[80%]">
<ToolCallCard
toolCall={tc}
isExpanded={expandedToolCalls.has(tc.toolCallId)}
onToggle={() => toggleToolCall(tc.toolCallId)}
/>
</div>
</div>
))}
</div>
)}
{streamingContent && (
<div className="flex justify-start">
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
<Streamdown>{streamingContent}</Streamdown>
</div>
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
<div ref={messagesContainerRef} className="h-full overflow-y-auto">
<div className="max-w-4xl mx-auto">
{/* Messages */}
{messages.map((message) => (
<div key={message.id} className="px-4 py-2">
{/* USER message */}
{message.role === 'USER' && (
<div className="flex justify-end">
<div className="max-w-[80%] rounded-lg p-4 bg-primary text-primary-foreground">
<div className="text-sm whitespace-pre-wrap">{message.content}</div>
</div>
</div>
)}
{isSending && !streamingContent && toolCalls.length === 0 && (
{/* ASSISTANT message */}
{message.role === 'ASSISTANT' && message.content && (
<div className="flex justify-start">
<div className="rounded-lg p-4 bg-surface-secondary">
<div className="flex items-center gap-2 text-foreground-secondary">
<Icon icon="loader" size="sm" className="animate-spin" />
<span className="text-sm">Omega is thinking...</span>
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
<Streamdown>{message.content}</Streamdown>
</div>
{/* Token usage for debugging */}
{(message.inputTokens || message.outputTokens) && (
<div className="mt-2 pt-2 border-t border-border/50 text-[10px] text-foreground-tertiary font-mono">
{message.inputTokens && <span>In: {message.inputTokens}</span>}
{message.inputTokens && message.outputTokens && <span> | </span>}
{message.outputTokens && <span>Out: {message.outputTokens}</span>}
</div>
)}
</div>
</div>
)}
{/* TOOL message - show as collapsed tool result */}
{message.role === 'TOOL' && (
<div className="flex justify-start">
<div className="max-w-[80%]">
<div className="text-xs text-foreground-tertiary mb-1">
Tool Results
</div>
<pre className="text-xs font-mono bg-surface-secondary border border-border rounded p-2 overflow-x-auto max-h-32 overflow-y-auto">
{message.content}
</pre>
</div>
</div>
)}
</div>
),
}}
itemContent={(_index, message) => (
<div className="px-4 py-2 max-w-4xl mx-auto">
{/* USER message */}
{message.role === 'USER' && (
<div className="flex justify-end">
<div className="max-w-[80%] rounded-lg p-4 bg-primary text-primary-foreground">
<div className="text-sm whitespace-pre-wrap">{message.content}</div>
</div>
</div>
)}
))}
{/* ASSISTANT message */}
{message.role === 'ASSISTANT' && message.content && (
{/* Live tool calls during streaming */}
{toolCalls.length > 0 && (
<div className="px-4 pb-4 space-y-2">
{toolCalls.map((tc) => (
<div key={tc.toolCallId} className="flex justify-start">
<div className="max-w-[80%]">
<ToolCallCard
toolCall={tc}
isExpanded={expandedToolCalls.has(tc.toolCallId)}
onToggle={() => toggleToolCall(tc.toolCallId)}
/>
</div>
</div>
))}
</div>
)}
{/* Streaming content */}
{streamingContent && (
<div className="px-4 pb-4">
<div className="flex justify-start">
<div className="max-w-[80%] rounded-lg p-4 bg-surface-secondary">
<div className="text-sm prose prose-sm dark:prose-invert max-w-none">
<Streamdown>{message.content}</Streamdown>
<Streamdown>{streamingContent}</Streamdown>
</div>
{/* Token usage for debugging */}
{(message.inputTokens || message.outputTokens) && (
<div className="mt-2 pt-2 border-t border-border/50 text-[10px] text-foreground-tertiary font-mono">
{message.inputTokens && <span>In: {message.inputTokens}</span>}
{message.inputTokens && message.outputTokens && <span> | </span>}
{message.outputTokens && <span>Out: {message.outputTokens}</span>}
</div>
)}
<span className="inline-block w-2 h-4 bg-primary animate-pulse ml-1" />
</div>
</div>
)}
</div>
)}
{/* TOOL message - show as collapsed tool result */}
{message.role === 'TOOL' && (
{/* Thinking indicator */}
{isSending && !streamingContent && toolCalls.length === 0 && (
<div className="px-4 pb-4">
<div className="flex justify-start">
<div className="max-w-[80%]">
<div className="text-xs text-foreground-tertiary mb-1">
Tool Results
<div className="rounded-lg p-4 bg-surface-secondary">
<div className="flex items-center gap-2 text-foreground-secondary">
<Icon icon="loader" size="sm" className="animate-spin" />
<span className="text-sm">Omega is thinking...</span>
</div>
<pre className="text-xs font-mono bg-surface-secondary border border-border rounded p-2 overflow-x-auto max-h-32 overflow-y-auto">
{message.content}
</pre>
</div>
</div>
)}
</div>
)}
/>
</div>
)}
</div>
</div>
)}
</div>

View file

@ -4,49 +4,32 @@
* Defines the default behavior for the Omega AI agent.
*/
export const OMEGA_SYSTEM_PROMPT = `You are Omega, an AI assistant with access to tools from the TPMJS registry - a collection of AI-ready tools that can help you complete tasks.
export const OMEGA_SYSTEM_PROMPT = `You are Omega, an AI assistant powered by the TPMJS tool registry - a collection of 1M+ AI-ready tools.
## Your Capabilities
## How It Works
You have access to two special tools:
When you receive a message, relevant tools are automatically loaded based on the user's request. You'll see a list of available tools in each response - USE THEM.
1. **registrySearch** - Search the TPMJS tool registry to find tools that can help complete a task. Returns tool metadata including toolId for use with registryExecute.
2. **registryExecute** - Execute a tool from the registry by its toolId with the required parameters. Tools run in a secure sandbox.
## Your Job
## How to Help Users
When a user asks you to complete a task:
1. **Analyze the request** - Understand what the user wants to accomplish
2. **Search for tools** - Use registrySearch to find relevant tools that can help
3. **Select the best tool(s)** - Choose the most appropriate tool(s) based on the search results and their toolIds
4. **Execute tools** - Use registryExecute to run the selected tools with the correct parameters (toolId and params)
5. **Synthesize results** - Combine tool outputs into a helpful, clear response
1. **Look at the available tools** - They've been selected based on what the user asked for
2. **Call the appropriate tool(s)** - Don't just describe what they do, actually use them
3. **Explain the results** - After a tool returns, summarize what happened for the user
## Best Practices
- **Be transparent** - Always explain which tools you're using and why
- **Handle errors gracefully** - If a tool fails, explain what went wrong and try alternatives
- **Validate inputs** - Make sure you have all required parameters before executing a tool
- **Iterate when needed** - Some tasks may require multiple tool executions
- **Ask for clarification** - If the user's request is ambiguous, ask questions before proceeding
- **Take action** - If a tool can help, call it immediately
- **Be transparent** - Tell the user which tool you're using
- **Handle errors** - If a tool fails, explain and try an alternative
- **Ask for clarity** - If you need more info, ask before proceeding
## Tool Search Tips
## Response Style
When searching for tools:
- Use descriptive keywords related to the task (e.g., "web scraping", "image processing", "API call")
- If you don't find the right tool, try different search terms
- Consider the tool's input schema to ensure you can provide the required parameters
- Keep responses concise and helpful
- Present tool outputs in a clear, readable format
- Offer to do more if the user might need it
## Response Format
When presenting results:
- Summarize what you did and what tools you used
- Present the output in a clear, readable format
- If the output is large, highlight the most relevant parts
- Offer to do more or explain further if needed
Remember: You're here to help users accomplish tasks efficiently by leveraging the vast TPMJS tool ecosystem. Be helpful, be clear, and be thorough.`;
Remember: Your value is in EXECUTING tools to get real results, not describing what tools could do.`;
/**
* Generate a custom system prompt with user preferences

6
pnpm-lock.yaml generated
View file

@ -266,12 +266,6 @@ importers:
'@tpmjs/package-executor':
specifier: workspace:*
version: link:../../packages/package-executor
'@tpmjs/registry-execute':
specifier: workspace:*
version: link:../../packages/tools/registryExecute
'@tpmjs/registry-search':
specifier: workspace:*
version: link:../../packages/tools/registrySearch
'@tpmjs/types':
specifier: workspace:*
version: link:../../packages/types