From 7b5f7e8f97889c0065a54fa5ce6af3065111efcb Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 24 Jun 2025 20:06:20 -0500 Subject: [PATCH] feat: improve web examples --- .../api-v1/deployments/create-deployment.ts | 5 +- .../api/src/api-v1/projects/create-project.ts | 5 +- apps/api/src/lib/create-consumer-token.ts | 4 +- apps/web/src/app/page.tsx | 3 +- apps/web/src/components/code-block.tsx | 72 ++++- .../src/{app => components}/example-usage.tsx | 111 ++++++-- .../loading-indicator/styles.module.css | 1 + apps/web/src/lib/developer-config.ts | 269 ++++++++++++++---- .../tool-client/src/agentic-tool-client.ts | 8 +- readme.md | 2 + 10 files changed, 391 insertions(+), 89 deletions(-) rename apps/web/src/{app => components}/example-usage.tsx (64%) diff --git a/apps/api/src/api-v1/deployments/create-deployment.ts b/apps/api/src/api-v1/deployments/create-deployment.ts index c7f07ed4..508e571a 100644 --- a/apps/api/src/api-v1/deployments/create-deployment.ts +++ b/apps/api/src/api-v1/deployments/create-deployment.ts @@ -79,7 +79,10 @@ export function registerV1CreateDeployment( if (!project) { // Used for testing e2e fixtures in the development marketplace - const isPrivate = !(user.username === 'dev' && env.isDev) + const isPrivate = !( + (user.username === 'dev' && env.isDev) || + user.username === 'agentic' + ) // Upsert the project if it doesn't already exist // The typecast is necessary here because we're not populating the diff --git a/apps/api/src/api-v1/projects/create-project.ts b/apps/api/src/api-v1/projects/create-project.ts index be89213b..47729128 100644 --- a/apps/api/src/api-v1/projects/create-project.ts +++ b/apps/api/src/api-v1/projects/create-project.ts @@ -63,7 +63,10 @@ export function registerV1CreateProject( ) // Used for testing e2e fixtures in the development marketplace - const isPrivate = !(user.username === 'dev' && env.isDev) + const isPrivate = !( + (user.username === 'dev' && env.isDev) || + user.username === 'agentic' + ) const [project] = await db .insert(schema.projects) diff --git a/apps/api/src/lib/create-consumer-token.ts b/apps/api/src/lib/create-consumer-token.ts index ede94852..ae2ddf73 100644 --- a/apps/api/src/lib/create-consumer-token.ts +++ b/apps/api/src/lib/create-consumer-token.ts @@ -1,7 +1,5 @@ import { sha256 } from '@agentic/platform-core' export async function createConsumerToken(): Promise { - const hash = await sha256() - - return hash.slice(0, 24) + return sha256() } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 4fb03037..e406236d 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,11 +1,10 @@ import Link from 'next/link' import { DemandSideCTA } from '@/components/demand-side-cta' +import { ExampleUsage } from '@/components/example-usage' import { GitHubStarCounter } from '@/components/github-star-counter' import { githubUrl, twitterUrl } from '@/lib/config' -import { ExampleUsage } from './example-usage' - export default function TheBestDamnLandingPageEver() { return ( <> diff --git a/apps/web/src/components/code-block.tsx b/apps/web/src/components/code-block.tsx index 79018b40..620aaa74 100644 --- a/apps/web/src/components/code-block.tsx +++ b/apps/web/src/components/code-block.tsx @@ -1,9 +1,21 @@ import { toJsxRuntime } from 'hast-util-to-jsx-runtime' -import { Fragment, type JSX, useEffect, useState } from 'react' +import { CheckIcon, CopyIcon } from 'lucide-react' +import { + Fragment, + type JSX, + useCallback, + useEffect, + useRef, + useState +} from 'react' import { jsx, jsxs } from 'react/jsx-runtime' import { type BundledLanguage, codeToHast } from 'shiki/bundle/web' +import { toastError } from '@/lib/notifications' +import { cn } from '@/lib/utils' + import { LoadingIndicator } from './loading-indicator' +import { Button } from './ui/button' export async function highlight({ code, @@ -51,10 +63,62 @@ export function CodeBlock({ className?: string }) { const [nodes, setNodes] = useState(initial) + const [isCopied, setIsCopied] = useState(false) + const timeoutRef = useRef | null>(null) useEffect(() => { - void highlight({ code, lang, theme, className }).then(setNodes) - }, [code, lang, theme, className]) + void highlight({ + code, + lang, + theme, + className: 'rounded-sm w-full text-wrap p-4 text-sm' + }).then(setNodes) + }, [code, lang, theme]) - return nodes ?? + const onCopy = useCallback(() => { + ;(async () => { + try { + await navigator.clipboard.writeText(code) + setIsCopied(true) + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + + timeoutRef.current = setTimeout(() => { + timeoutRef.current = null + setIsCopied(false) + }, 2000) + } catch { + setIsCopied(true) + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + void toastError('Error copying code to clipboard') + } + })() + }, [code, timeoutRef]) + + return ( +
+ {nodes ? ( + <> + {nodes} + + + + ) : ( + + )} +
+ ) } diff --git a/apps/web/src/app/example-usage.tsx b/apps/web/src/components/example-usage.tsx similarity index 64% rename from apps/web/src/app/example-usage.tsx rename to apps/web/src/components/example-usage.tsx index 8cc0c6ff..c19ced41 100644 --- a/apps/web/src/app/example-usage.tsx +++ b/apps/web/src/components/example-usage.tsx @@ -2,11 +2,13 @@ import { useLocalStorage } from 'react-use' +import { useAgentic } from '@/components/agentic-provider' import { CodeBlock } from '@/components/code-block' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { defaultConfig, type DeveloperConfig, + getCodeForDeveloperConfig, type HTTPTarget, httpTargetLabels, httpTargets, @@ -23,16 +25,85 @@ import { tsFrameworkTargetLabels, tsFrameworkTargets } from '@/lib/developer-config' +import { useQuery } from '@/lib/query-client' + +import { LoadingIndicator } from './loading-indicator' export function ExampleUsage() { + const ctx = useAgentic() + const [config, setConfig] = useLocalStorage( 'config', defaultConfig ) + // TODO: make this configurable + // TODO: allow to take the project and/or consumer in as props + // TODO: need a way of fetching a project and target deployment; same as in `AgenticToolClient.fromIdentifier` (currently only supports latest) + const projectIdentifier = '@agentic/search' + + // Load the public project + const { + data: project, + isLoading, + isError + } = useQuery({ + queryKey: ['project', projectIdentifier], + queryFn: () => + ctx!.api.getPublicProjectByIdentifier({ + projectIdentifier, + populate: ['lastPublishedDeployment'] + }), + enabled: !!ctx + }) + + // If the user is authenticated, check if they have an active subscription to + // this project + // TODO: use consumer for apiKey + // const { + // data: consumer, + // isLoading: isConsumerLoading + // // isError: isConsumerError + // } = useQuery({ + // queryKey: [ + // 'project', + // projectIdentifier, + // 'user', + // ctx?.api.authSession?.user.id + // ], + // queryFn: () => + // ctx!.api.getConsumerByProjectIdentifier({ + // projectIdentifier + // }), + // enabled: !!ctx?.isAuthenticated + // }) + + if (isLoading || !config) { + return + } + + // TODO: allow to target a specific deployment + const deployment = project?.lastPublishedDeployment + + if (isError || !project || !deployment) { + return ( +
+ Error loading project. Please refresh the page or contact{' '} + support@agentic.so. +
+ ) + } + + const codeSnippet = getCodeForDeveloperConfig({ + config, + project, + deployment, + identifier: projectIdentifier + }) + return ( setConfig({ ...defaultConfig, @@ -40,7 +111,7 @@ export function ExampleUsage() { target: value as Target }) } - className='w-full max-w-2xl' + className='w-full max-w-3xl' > {targets.map((target) => ( @@ -52,7 +123,7 @@ export function ExampleUsage() { setConfig({ ...defaultConfig, @@ -80,11 +151,7 @@ export function ExampleUsage() { value={mcpClientTarget} className='w-full' > - + ))} @@ -92,7 +159,7 @@ export function ExampleUsage() { setConfig({ ...defaultConfig, @@ -102,12 +169,12 @@ export function ExampleUsage() { } className='w-full' > - + {tsFrameworkTargets.map((framework) => ( {tsFrameworkTargetLabels[framework]} @@ -116,11 +183,7 @@ export function ExampleUsage() { {tsFrameworkTargets.map((framework) => ( - + ))} @@ -128,7 +191,7 @@ export function ExampleUsage() { setConfig({ ...defaultConfig, @@ -152,11 +215,7 @@ export function ExampleUsage() { {pyFrameworkTargets.map((framework) => ( - + ))} @@ -164,7 +223,7 @@ export function ExampleUsage() { setConfig({ ...defaultConfig, @@ -188,11 +247,7 @@ export function ExampleUsage() { {httpTargets.map((httpTarget) => ( - + ))} diff --git a/apps/web/src/components/loading-indicator/styles.module.css b/apps/web/src/components/loading-indicator/styles.module.css index 106b53e8..7efc2a67 100644 --- a/apps/web/src/components/loading-indicator/styles.module.css +++ b/apps/web/src/components/loading-indicator/styles.module.css @@ -5,6 +5,7 @@ flex-direction: column; justify-content: center; align-items: center; + margin: 0 auto; } .fill { diff --git a/apps/web/src/lib/developer-config.ts b/apps/web/src/lib/developer-config.ts index 0310189a..4e895e9d 100644 --- a/apps/web/src/lib/developer-config.ts +++ b/apps/web/src/lib/developer-config.ts @@ -1,4 +1,5 @@ import type { Deployment, Project } from '@agentic/platform-types' +import type { BundledLanguage } from 'shiki/bundle/web' import { assert } from '@agentic/platform-core' import { gatewayBaseUrl } from './config' @@ -24,7 +25,7 @@ export const httpTargets: (keyof typeof httpTargetLabels)[] = Object.keys( export type HTTPTarget = (typeof httpTargets)[number] export const mcpClientTargetLabels = { - any: 'Any MCP Client', + url: 'MCP Server URL', 'claude-desktop': 'Claude Desktop', raycast: 'Raycast', cursor: 'Cursor', @@ -41,8 +42,8 @@ export const tsFrameworkTargetLabels = { 'openai-chat': 'OpenAI Chat', 'openai-responses': 'OpenAI Responses', langchain: 'LangChain', - mastra: 'Mastra', llamaindex: 'LlamaIndex', + mastra: 'Mastra', 'firebase-genkit': 'Firebase GenKit', xsai: 'xsAI' } as const @@ -69,19 +70,25 @@ export type DeveloperConfig = { export const defaultConfig: DeveloperConfig = { target: 'typescript', - mcpClientTarget: 'any', + mcpClientTarget: 'url', tsFrameworkTarget: 'ai', pyFrameworkTarget: 'openai', httpTarget: 'curl' } +export type CodeSnippet = { + code: string + lang: BundledLanguage + // install?: string // TODO +} + export function getCodeForDeveloperConfig(opts: { config: DeveloperConfig project: Project deployment: Deployment identifier: string tool?: string -}): string { +}): CodeSnippet { const { config } = opts switch (config.target) { @@ -90,8 +97,7 @@ export function getCodeForDeveloperConfig(opts: { case 'typescript': return getCodeForTSFrameworkConfig(opts) case 'python': - return 'Python support is coming soon...' - // return getCodeForPythonFrameworkConfig(opts) + return getCodeForPythonFrameworkConfig(opts) case 'http': return getCodeForHTTPConfig(opts) } @@ -101,21 +107,27 @@ export function getCodeForMCPClientConfig({ identifier }: { identifier: string -}): string { - return `${gatewayBaseUrl}/${identifier}/mcp` +}): CodeSnippet { + return { + code: `${gatewayBaseUrl}/${identifier}/mcp`, + lang: 'bash' + } } export function getCodeForTSFrameworkConfig({ config, - identifier + identifier, + prompt = 'What is the latest news about AI?' }: { config: DeveloperConfig identifier: string -}): string { + prompt?: string +}): CodeSnippet { switch (config.tsFrameworkTarget) { case 'ai': - return ` -import { createAISDKTools } from '@agentic/ai' + return { + code: ` +import { createAISDKTools } from '@agentic/ai-sdk' import { AgenticToolClient } from '@agentic/platform-tool-client' import { openai } from '@ai-sdk/openai' import { generateText } from 'ai' @@ -126,66 +138,221 @@ const result = await generateText({ model: openai('gpt-4o-mini'), tools: createAISDKTools(searchTool), toolChoice: 'required', - temperature: 0, - system: 'You are a helpful assistant. Be as concise as possible.', - prompt: 'What is the latest news about AI?' + prompt: '${prompt}' }) console.log(result.toolResults[0]) - `.trim() + `.trim(), + lang: 'ts' + } case 'openai-chat': - return ` + return { + code: ` import { AgenticToolClient } from '@agentic/platform-tool-client' import OpenAI from 'openai' const openai = new OpenAI() const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') +// This example uses OpenAI's Chat Completions API const res = await openai.chat.completions.create({ + model: 'gpt-4o-mini', messages: [ - { - role: 'system', - content: 'You are a helpful assistant. Be as concise as possible.' - } { role: 'user', - content: 'What is the latest news about AI?' + content: '${prompt}' } ], - model: 'gpt-4o-mini', - temperature: 0, tools: searchTool.functions.toolSpecs, tool_choice: 'required' }) const message = res.choices[0]!.message! -const toolCall = message.tool_calls![0]! - -const tool = searchTool.functions.get(toolCall.function.name)! -const toolResult = await tool(toolCall.function.arguments) +const toolCall = message.tool_calls![0]!.function! +const toolResult = await searchTool.callTool(toolCall.name, toolCall.arguments) console.log(toolResult) -`.trim() - } +`.trim(), + lang: 'ts' + } - return '' + case 'openai-responses': + return { + code: ` +import { AgenticToolClient } from '@agentic/platform-tool-client' +import OpenAI from 'openai' + +const openai = new OpenAI() +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +// This example uses OpenAI's newer Responses API +const res = await openai.responses.create({ + model: 'gpt-4o-mini', + tools: searchTool.functions.responsesToolSpecs, + tool_choice: 'required', + input: [ + { + role: 'user', + content: '${prompt}' + } + ] +}) + +const toolCall = res.output[0] +const toolResult = await searchTool.callTool(toolCall.name, toolCall.arguments) + +console.log(toolResult) +`.trim(), + lang: 'ts' + } + + case 'langchain': + return { + code: ` +import { createLangChainTools } from '@agentic/langchain' +import { AgenticToolClient } from '@agentic/platform-tool-client' +import { ChatPromptTemplate } from '@langchain/core/prompts' +import { ChatOpenAI } from '@langchain/openai' +import { AgentExecutor, createToolCallingAgent } from 'langchain/agents' + +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +const agent = createToolCallingAgent({ + llm: new ChatOpenAI({ model: 'gpt-4o-mini' }), + tools: createLangChainTools(searchTool), + prompt: ChatPromptTemplate.fromMessages([ + ['placeholder', '{chat_history}'], + ['human', '{input}'], + ['placeholder', '{agent_scratchpad}'] + ]) +}) + +const agentExecutor = new AgentExecutor({ agent, tools }) + +const result = await agentExecutor.invoke({ + input: '${prompt}' +}) + +console.log(result.output) + `.trim(), + lang: 'ts' + } + + case 'llamaindex': + return { + code: ` +import { createLlamaIndexTools } from '@agentic/llamaindex' +import { AgenticToolClient } from '@agentic/platform-tool-client' +import { openai } from '@llamaindex/openai' +import { agent } from '@llamaindex/workflow' + +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +const exampleAgent = agent({ + llm: openai({ model: 'gpt-4o-mini', temperature: 0 }), + tools: createLlamaIndexTools(searchTool) +}) + +const response = await exampleAgent.run( + '${prompt}' +) + +console.log(response.data.result) + `.trim(), + lang: 'ts' + } + + case 'mastra': + return { + code: ` +import { createMastraTools } from '@agentic/mastra' +import { AgenticToolClient } from '@agentic/platform-tool-client' +import { openai } from '@ai-sdk/openai' +import { Agent } from '@mastra/core/agent' + +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +const exampleAgent = new Agent({ + name: 'Example Agent', + model: openai('gpt-4o-mini') as any, + instructions: 'You are a helpful assistant. Be as concise as possible.', + tools: createMastraTools(searchTool) +}) + +const res = await exampleAgent.generate( + '${prompt}' +) + +console.log(res.text)`.trim(), + lang: 'ts' + } + + case 'firebase-genkit': + return { + code: ` +import { createGenkitTools } from '@agentic/genkit' +import { AgenticToolClient } from '@agentic/platform-tool-client' +import { genkit } from 'genkit' +import { gpt4oMini, openAI } from 'genkitx-openai' + +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +const ai = genkit({ + plugins: [openAI()] +}) + +const result = await ai.generate({ + model: gpt4oMini, + tools: createGenkitTools(ai, searchTool), + prompt: '${prompt}' +}) + +console.log(result)`.trim(), + lang: 'ts' + } + + case 'xsai': + return { + code: ` +import { AgenticToolClient } from '@agentic/platform-tool-client' +import { createXSAITools } from '@agentic/xsai' +import { generateText } from 'xsai' + +const searchTool = await AgenticToolClient.fromIdentifier('${identifier}') + +const result = await generateText({ + apiKey: process.env.OPENAI_API_KEY!, + baseURL: 'https://api.openai.com/v1/', + model: 'gpt-4o-mini', + tools: await createXSAITools(searchTool), + toolChoice: 'required', + messages: [ + { + role: 'user', + content: '${prompt}' + } + ] +}) + +console.log(JSON.stringify(result, null, 2))`.trim(), + lang: 'ts' + } + } } -// export function getCodeForPythonFrameworkConfig({ -// config, -// project, -// deployment, -// tool -// }: { -// config: DeveloperConfig -// project: Project -// deployment: Deployment -// identifier: string -// tool?: string -// }): string { -// return '' -// } +export function getCodeForPythonFrameworkConfig(_opts: { + config: DeveloperConfig + project: Project + deployment: Deployment + identifier: string + tool?: string +}): CodeSnippet { + return { + code: 'Python SDK is coming soon. For now, use the MCP or HTTP examples', + lang: 'md' + } +} export function getCodeForHTTPConfig({ config, @@ -197,7 +364,7 @@ export function getCodeForHTTPConfig({ deployment: Deployment identifier: string tool?: string -}): string { +}): CodeSnippet { tool ??= deployment.tools[0]?.name assert(tool, 'tool is required') // TODO: need a way of getting example tool args @@ -206,9 +373,15 @@ export function getCodeForHTTPConfig({ switch (config.httpTarget) { case 'curl': - return `curl -X POST -H "Content-Type: application/json" -d '{"query": "example google search"}' ${url}` + return { + code: `curl -X POST -H "Content-Type: application/json" -d '{"query": "example google search"}' ${url}`, + lang: 'bash' + } case 'httpie': - return `http -j ${url} query='example google search'` + return { + code: `http -j ${url} query='example google search'`, + lang: 'bash' + } } } diff --git a/packages/tool-client/src/agentic-tool-client.ts b/packages/tool-client/src/agentic-tool-client.ts index 5336bb8c..c81a3007 100644 --- a/packages/tool-client/src/agentic-tool-client.ts +++ b/packages/tool-client/src/agentic-tool-client.ts @@ -73,6 +73,12 @@ export class AgenticToolClient extends AIFunctionsProvider { return this._functions } + async callTool(toolName: string, args: string | Record) { + const tool = this.functions.get(toolName) + assert(tool, `Tool "${toolName}" not found`) + return tool(typeof args === 'string' ? args : JSON.stringify(args)) + } + /** * Creates an Agentic tool client from a project or deployment identifier. * @@ -84,8 +90,6 @@ export class AgenticToolClient extends AIFunctionsProvider { * @example * ```ts * const searchTool = await AgenticToolClient.fromIdentifier('@agentic/search') - * const searchToolV1 = await AgenticToolClient.fromIdentifier('@agentic/search@v1.0.0') - * const searchToolLatest = await AgenticToolClient.fromIdentifier('@agentic/search@latest') * ``` */ static async fromIdentifier( diff --git a/readme.md b/readme.md index 634150ef..de8641a0 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,8 @@ - consider using [neon serverless driver](https://orm.drizzle.team/docs/connect-neon) for production - can this also be used locally? - may need to update our `drizzle-orm` fork +- simplify `AgenticToolClient` and only require one package per TS LLM SDK + - `createAISDKToolsFromIdentifier(projectIdentifier)` ## TODO: Post-MVP