diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 38387b7be..0fdda693c 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -1,6 +1,7 @@ import "server-only"; -import type { AgentIntegrationConfig } from "./types/integration"; +import type { AbstractAgent } from "@ag-ui/client"; +import { FeaturesFor, IntegrationId } from "./menu"; import { MiddlewareStarterAgent } from "@ag-ui/middleware-starter"; import { ServerStarterAgent } from "@ag-ui/server-starter"; import { ServerStarterAllFeaturesAgent } from "@ag-ui/server-starter-all-features"; @@ -25,453 +26,286 @@ import { A2AClient } from "@a2a-js/sdk/client"; import { LangChainAgent } from "@ag-ui/langchain"; const envVars = getEnvVars(); -export const agentsIntegrations: AgentIntegrationConfig[] = [ - { - id: "middleware-starter", - agents: async () => { - return { - agentic_chat: new MiddlewareStarterAgent(), - }; - }, - }, - { - id: "pydantic-ai", - agents: async () => { - return { - agentic_chat: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/agentic_chat/`, - }), - agentic_generative_ui: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/agentic_generative_ui/`, - }), - human_in_the_loop: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/human_in_the_loop/`, - }), + +/** + * Helper to map feature keys to agent instances using a builder function. + * Reduces repetition when all agents follow the same pattern with different paths. + * + * Uses `const` type parameter to preserve exact literal keys from the mapping. + * The return type `{ -readonly [K in keyof T]: AbstractAgent }` removes the readonly + * modifier added by `const T` to match the expected AgentsMap type. + */ +function mapAgents>( + builder: (path: string) => AbstractAgent, + mapping: T +): { -readonly [K in keyof T]: AbstractAgent } { + return Object.fromEntries( + Object.entries(mapping).map(([key, path]) => [key, builder(path)]) + ) as { -readonly [K in keyof T]: AbstractAgent }; +} + +/** + * Agent integrations map - keys are integration IDs from menu.ts + * TypeScript enforces: + * - All integration IDs from menu.ts must have an entry + * - All features for each integration must be present in the returned object + */ +type AgentsMap = { + [K in IntegrationId]: () => Promise<{ [P in FeaturesFor]: AbstractAgent }>; +}; + +export const agentsIntegrations = { + "middleware-starter": async () => ({ + agentic_chat: new MiddlewareStarterAgent(), + }), + + "pydantic-ai": async () => + mapAgents( + (path) => new PydanticAIAgent({ url: `${envVars.pydanticAIUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + agentic_generative_ui: "agentic_generative_ui", + human_in_the_loop: "human_in_the_loop", // Disabled until we can figure out why production builds break - // predictive_state_updates: new PydanticAIAgent({ - // url: `${envVars.pydanticAIUrl}/predictive_state_updates/`, - // }), - shared_state: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/shared_state/`, - }), - tool_based_generative_ui: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/tool_based_generative_ui/`, - }), - backend_tool_rendering: new PydanticAIAgent({ - url: `${envVars.pydanticAIUrl}/backend_tool_rendering`, - }), - }; - }, - }, - { - id: "server-starter", - agents: async () => { - return { - agentic_chat: new ServerStarterAgent({ url: envVars.serverStarterUrl }), - }; - }, - }, - { - id: "adk-middleware", - agents: async () => { - return { - agentic_chat: new ADKAgent({ url: `${envVars.adkMiddlewareUrl}/chat` }), - agentic_generative_ui: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/adk-agentic-generative-ui/`, - }), - tool_based_generative_ui: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/adk-tool-based-generative-ui`, - }), - human_in_the_loop: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/adk-human-in-loop-agent`, - }), - backend_tool_rendering: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/backend_tool_rendering`, - }), - shared_state: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/adk-shared-state-agent`, - }), - predictive_state_updates: new ADKAgent({ - url: `${envVars.adkMiddlewareUrl}/adk-predictive-state-agent`, - }), - }; - }, - }, - { - id: "server-starter-all-features", - agents: async () => { - return { - agentic_chat: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/agentic_chat`, - }), - backend_tool_rendering: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/backend_tool_rendering`, - }), - human_in_the_loop: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/human_in_the_loop`, - }), - agentic_generative_ui: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/agentic_generative_ui`, - }), - tool_based_generative_ui: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/tool_based_generative_ui`, - }), - shared_state: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/shared_state`, - }), - predictive_state_updates: new ServerStarterAllFeaturesAgent({ - url: `${envVars.serverStarterAllFeaturesUrl}/predictive_state_updates`, - }), - }; - }, - }, - { - id: "mastra", - agents: async () => { - const mastraClient = new MastraClient({ - baseUrl: envVars.mastraUrl, - }); + // predictive_state_updates: "predictive_state_updates", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + backend_tool_rendering: "backend_tool_rendering", + } + ), - return MastraAgent.getRemoteAgents({ - mastraClient, - }); - }, - }, - { - id: "mastra-agent-local", - agents: async () => { - return MastraAgent.getLocalAgents({ mastra }); - }, + "server-starter": async () => ({ + agentic_chat: new ServerStarterAgent({ url: envVars.serverStarterUrl }), + }), + + "adk-middleware": async () => + mapAgents( + (path) => new ADKAgent({ url: `${envVars.adkMiddlewareUrl}/${path}` }), + { + agentic_chat: "chat", + agentic_generative_ui: "adk-agentic-generative-ui", + tool_based_generative_ui: "adk-tool-based-generative-ui", + human_in_the_loop: "adk-human-in-loop-agent", + backend_tool_rendering: "backend_tool_rendering", + shared_state: "adk-shared-state-agent", + predictive_state_updates: "adk-predictive-state-agent", + } + ), + + "server-starter-all-features": async () => + mapAgents( + (path) => new ServerStarterAllFeaturesAgent({ url: `${envVars.serverStarterAllFeaturesUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + // TODO: Add agent for agentic_chat_reasoning + backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + agentic_generative_ui: "agentic_generative_ui", + tool_based_generative_ui: "tool_based_generative_ui", + shared_state: "shared_state", + predictive_state_updates: "predictive_state_updates", + } + ), + + mastra: async () => { + const mastraClient = new MastraClient({ + baseUrl: envVars.mastraUrl, + }); + + return MastraAgent.getRemoteAgents({ + mastraClient, + keys: ["agentic_chat", "backend_tool_rendering", "human_in_the_loop", "tool_based_generative_ui"], + }); }, + + "mastra-agent-local": async () => + MastraAgent.getLocalAgents({ + mastra, + keys: ["agentic_chat", "backend_tool_rendering", "human_in_the_loop", "shared_state", "tool_based_generative_ui"], + }), + // Disabled until we can support Vercel AI SDK v5 - // { - // id: "vercel-ai-sdk", - // agents: async () => { - // return { - // agentic_chat: new VercelAISDKAgent({ model: openai("gpt-4o") }), - // }; - // }, - // }, - { - id: "langgraph", - agents: async () => { - return { - agentic_chat: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "agentic_chat", - }), - backend_tool_rendering: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "backend_tool_rendering", - }), - agentic_generative_ui: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "agentic_generative_ui", - }), - human_in_the_loop: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "human_in_the_loop", - }), - predictive_state_updates: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "predictive_state_updates", - }), - shared_state: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "shared_state", - }), - tool_based_generative_ui: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "tool_based_generative_ui", - }), - agentic_chat_reasoning: new LangGraphHttpAgent({ - url: `${envVars.langgraphPythonUrl}/agent/agentic_chat_reasoning`, - }), - subgraphs: new LangGraphAgent({ - deploymentUrl: envVars.langgraphPythonUrl, - graphId: "subgraphs", - }), - }; - }, - }, - { - id: "langgraph-fastapi", - agents: async () => { - return { - agentic_chat: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/agentic_chat`, - }), - backend_tool_rendering: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/backend_tool_rendering`, - }), - agentic_generative_ui: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/agentic_generative_ui`, - }), - human_in_the_loop: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/human_in_the_loop`, - }), - predictive_state_updates: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/predictive_state_updates`, - }), - shared_state: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/shared_state`, - }), - tool_based_generative_ui: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/tool_based_generative_ui`, - }), - agentic_chat_reasoning: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/agentic_chat_reasoning`, - }), - subgraphs: new LangGraphHttpAgent({ - url: `${envVars.langgraphFastApiUrl}/agent/subgraphs`, - }), - }; - }, - }, - { - id: "langgraph-typescript", - agents: async () => { - return { - agentic_chat: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "agentic_chat", - }), - // agentic_chat_reasoning: new LangGraphAgent({ - // deploymentUrl: envVars.langgraphTypescriptUrl, - // graphId: "agentic_chat_reasoning", - // }), - agentic_generative_ui: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "agentic_generative_ui", - }), - human_in_the_loop: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "human_in_the_loop", - }), - predictive_state_updates: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "predictive_state_updates", - }), - shared_state: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "shared_state", - }), - tool_based_generative_ui: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "tool_based_generative_ui", - }), - subgraphs: new LangGraphAgent({ - deploymentUrl: envVars.langgraphTypescriptUrl, - graphId: "subgraphs", - }), - }; - }, - }, - { - id: "langchain", - agents: async () => { - return { - agentic_chat: new LangChainAgent({ - chainFn: async ({ messages, tools, threadId }) => { - // @ts-ignore - const { ChatOpenAI } = await import("@langchain/openai"); - const chatOpenAI = new ChatOpenAI({ model: "gpt-4o" }); - const model = chatOpenAI.bindTools(tools, { - strict: true, - }); - return model.stream(messages, { tools, metadata: { conversation_id: threadId } }); - }, - }), - tool_based_generative_ui: new LangChainAgent({ - chainFn: async ({ messages, tools, threadId }) => { - // @ts-ignore - const { ChatOpenAI } = await import("@langchain/openai"); - const chatOpenAI = new ChatOpenAI({ model: "gpt-4o" }); - const model = chatOpenAI.bindTools(tools, { - strict: true, - }); - return model.stream(messages, { tools, metadata: { conversation_id: threadId } }); - }, - }), + // "vercel-ai-sdk": async () => ({ + // agentic_chat: new VercelAISDKAgent({ model: openai("gpt-4o") }), + // }), + + langgraph: async () => ({ + ...mapAgents( + (graphId) => new LangGraphAgent({ deploymentUrl: envVars.langgraphPythonUrl, graphId }), + { + agentic_chat: "agentic_chat", + backend_tool_rendering: "backend_tool_rendering", + agentic_generative_ui: "agentic_generative_ui", + human_in_the_loop: "human_in_the_loop", + predictive_state_updates: "predictive_state_updates", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + subgraphs: "subgraphs", } - }, - }, - { - id: "agno", - agents: async () => { - return { - agentic_chat: new AgnoAgent({ - url: `${envVars.agnoUrl}/agentic_chat/agui`, - }), - tool_based_generative_ui: new AgnoAgent({ - url: `${envVars.agnoUrl}/tool_based_generative_ui/agui`, - }), - backend_tool_rendering: new AgnoAgent({ - url: `${envVars.agnoUrl}/backend_tool_rendering/agui`, - }), - human_in_the_loop: new AgnoAgent({ - url: `${envVars.agnoUrl}/human_in_the_loop/agui`, - }), - }; - }, - }, - { - id: "spring-ai", - agents: async () => { - return { - agentic_chat: new SpringAiAgent({ - url: `${envVars.springAiUrl}/agentic_chat/agui`, - }), - shared_state: new SpringAiAgent({ - url: `${envVars.springAiUrl}/shared_state/agui`, - }), - tool_based_generative_ui: new SpringAiAgent({ - url: `${envVars.springAiUrl}/tool_based_generative_ui/agui`, - }), - human_in_the_loop: new SpringAiAgent({ - url: `${envVars.springAiUrl}/human_in_the_loop/agui`, - }), - agentic_generative_ui: new SpringAiAgent({ - url: `${envVars.springAiUrl}/agentic_generative_ui/agui`, - }), - }; - }, - }, - { - id: "llama-index", - agents: async () => { - return { - agentic_chat: new LlamaIndexAgent({ - url: `${envVars.llamaIndexUrl}/agentic_chat/run`, - }), - human_in_the_loop: new LlamaIndexAgent({ - url: `${envVars.llamaIndexUrl}/human_in_the_loop/run`, - }), - agentic_generative_ui: new LlamaIndexAgent({ - url: `${envVars.llamaIndexUrl}/agentic_generative_ui/run`, - }), - shared_state: new LlamaIndexAgent({ - url: `${envVars.llamaIndexUrl}/shared_state/run`, - }), - backend_tool_rendering: new LlamaIndexAgent({ - url: `${envVars.llamaIndexUrl}/backend_tool_rendering/run`, - }), - }; - }, - }, - { - id: "crewai", - agents: async () => { - return { - agentic_chat: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/agentic_chat`, - }), - human_in_the_loop: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/human_in_the_loop`, - }), - tool_based_generative_ui: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/tool_based_generative_ui`, - }), - agentic_generative_ui: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/agentic_generative_ui`, - }), - shared_state: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/shared_state`, - }), - predictive_state_updates: new CrewAIAgent({ - url: `${envVars.crewAiUrl}/predictive_state_updates`, - }), - }; - }, - }, - { - id: "microsoft-agent-framework-python", - agents: async () => { - return { - agentic_chat: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/agentic_chat`, - }), - backend_tool_rendering: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/backend_tool_rendering`, - }), - human_in_the_loop: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/human_in_the_loop`, - }), - agentic_generative_ui: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/agentic_generative_ui`, - }), - shared_state: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/shared_state`, - }), - tool_based_generative_ui: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/tool_based_generative_ui`, - }), - predictive_state_updates: new HttpAgent({ - url: `${envVars.agentFrameworkPythonUrl}/predictive_state_updates`, - }), - }; - }, - }, - { - id: "a2a-basic", - agents: async () => { - const a2aClient = new A2AClient(envVars.a2aUrl); - return { - agentic_chat: new A2AAgent({ - description: "Direct A2A agent", - a2aClient, - debug: process.env.NODE_ENV !== "production", - }), - }; - }, - }, - { - id: "microsoft-agent-framework-dotnet", - agents: async () => { - return { - agentic_chat: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/agentic_chat`, - }), - backend_tool_rendering: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/backend_tool_rendering`, - }), - human_in_the_loop: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/human_in_the_loop`, - }), - agentic_generative_ui: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/agentic_generative_ui`, - }), - shared_state: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/shared_state`, - }), - tool_based_generative_ui: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/tool_based_generative_ui`, - }), - predictive_state_updates: new HttpAgent({ - url: `${envVars.agentFrameworkDotnetUrl}/predictive_state_updates`, - }), - }; - }, + ), + // Uses LangGraphHttpAgent instead of LangGraphAgent + agentic_chat_reasoning: new LangGraphHttpAgent({ + url: `${envVars.langgraphPythonUrl}/agent/agentic_chat_reasoning`, + }), + }), + + "langgraph-fastapi": async () => + mapAgents( + (path) => new LangGraphHttpAgent({ url: `${envVars.langgraphFastApiUrl}/agent/${path}` }), + { + agentic_chat: "agentic_chat", + backend_tool_rendering: "backend_tool_rendering", + agentic_generative_ui: "agentic_generative_ui", + human_in_the_loop: "human_in_the_loop", + predictive_state_updates: "predictive_state_updates", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + agentic_chat_reasoning: "agentic_chat_reasoning", + subgraphs: "subgraphs", + } + ), + + "langgraph-typescript": async () => + mapAgents( + (graphId) => new LangGraphAgent({ deploymentUrl: envVars.langgraphTypescriptUrl, graphId }), + { + agentic_chat: "agentic_chat", + // TODO: Add agent for backend_tool_rendering + agentic_generative_ui: "agentic_generative_ui", + human_in_the_loop: "human_in_the_loop", + predictive_state_updates: "predictive_state_updates", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + subgraphs: "subgraphs", + } + ), + + langchain: async () => + mapAgents( + new LangChainAgent({ + // TODO: @ranst91 - can you add param types here? + // @ts-expect-error - TODO: add types + chainFn: async ({ messages, tools, threadId }) => { + const { ChatOpenAI } = await import("@langchain/openai"); + const chatOpenAI = new ChatOpenAI({ model: "gpt-4o" }); + const model = chatOpenAI.bindTools(tools, { + strict: true, + }); + return model.stream(messages, { tools, metadata: { conversation_id: threadId } }); + }, + }), + { + agentic_chat: "", + tool_based_generative_ui: "", + } + ), + + agno: async () => + mapAgents( + (path) => new AgnoAgent({ url: `${envVars.agnoUrl}/${path}/agui` }), + { + agentic_chat: "agentic_chat", + tool_based_generative_ui: "tool_based_generative_ui", + backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + } + ), + + "spring-ai": async () => + mapAgents( + (path) => new SpringAiAgent({ url: `${envVars.springAiUrl}/${path}/agui` }), + { + agentic_chat: "agentic_chat", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + human_in_the_loop: "human_in_the_loop", + agentic_generative_ui: "agentic_generative_ui", + } + ), + + "llama-index": async () => + mapAgents( + (path) => new LlamaIndexAgent({ url: `${envVars.llamaIndexUrl}/${path}/run` }), + { + agentic_chat: "agentic_chat", + human_in_the_loop: "human_in_the_loop", + agentic_generative_ui: "agentic_generative_ui", + shared_state: "shared_state", + backend_tool_rendering: "backend_tool_rendering", + } + ), + + crewai: async () => + mapAgents( + (path) => new CrewAIAgent({ url: `${envVars.crewAiUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + // TODO: Add agent for backend_tool_rendering + // backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + tool_based_generative_ui: "tool_based_generative_ui", + agentic_generative_ui: "agentic_generative_ui", + shared_state: "shared_state", + predictive_state_updates: "predictive_state_updates", + } + ), + + "microsoft-agent-framework-python": async () => + mapAgents( + (path) => new HttpAgent({ url: `${envVars.agentFrameworkPythonUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + agentic_generative_ui: "agentic_generative_ui", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + predictive_state_updates: "predictive_state_updates", + } + ), + + "a2a-basic": async () => { + const a2aClient = new A2AClient(envVars.a2aUrl); + return { + vnext_chat: new A2AAgent({ + description: "Direct A2A agent", + a2aClient, + debug: process.env.NODE_ENV !== "production", + }), + }; }, - { - id: "a2a", - agents: async () => { - // A2A agents: building management, finance, it agents - const agentUrls = [ - envVars.a2aMiddlewareBuildingsManagementUrl, - envVars.a2aMiddlewareFinanceUrl, - envVars.a2aMiddlewareItUrl, - ]; - // AGUI orchestration/routing agent - const orchestrationAgent = new HttpAgent({ - url: envVars.a2aMiddlewareOrchestratorUrl, - }); - return { - a2a_chat: new A2AMiddlewareAgent({ - description: "Middleware that connects to remote A2A agents", - agentUrls, - orchestrationAgent, - instructions: ` + + "microsoft-agent-framework-dotnet": async () => + mapAgents( + (path) => new HttpAgent({ url: `${envVars.agentFrameworkDotnetUrl}/${path}` }), + { + agentic_chat: "agentic_chat", + backend_tool_rendering: "backend_tool_rendering", + human_in_the_loop: "human_in_the_loop", + agentic_generative_ui: "agentic_generative_ui", + shared_state: "shared_state", + tool_based_generative_ui: "tool_based_generative_ui", + predictive_state_updates: "predictive_state_updates", + } + ), + + a2a: async () => { + // A2A agents: building management, finance, it agents + const agentUrls = [ + envVars.a2aMiddlewareBuildingsManagementUrl, + envVars.a2aMiddlewareFinanceUrl, + envVars.a2aMiddlewareItUrl, + ]; + // AGUI orchestration/routing agent + const orchestrationAgent = new HttpAgent({ + url: envVars.a2aMiddlewareOrchestratorUrl, + }); + return { + a2a_chat: new A2AMiddlewareAgent({ + description: "Middleware that connects to remote A2A agents", + agentUrls, + orchestrationAgent, + instructions: ` You are an HR agent. You are responsible for hiring employees and other typical HR tasks. It's very important to contact all the departments necessary to complete the task. @@ -483,20 +317,21 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [ When choosing a seat with the buildings management agent, You MUST use the \`pickTable\` tool to have the user pick a seat. The buildings management agent will then use the \`pickSeat\` tool to pick a seat. `, - }), - }; - }, + }), + }; }, - { - id: "aws-strands", - agents: async () => { - return { - agentic_chat: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/agentic-chat/` }), - backend_tool_rendering: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/backend-tool-rendering/` }), - agentic_generative_ui: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/agentic-generative-ui/` }), - shared_state: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/shared-state/` }), - human_in_the_loop: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/human-in-the-loop/`, debug: true }), - }; - }, - }, -]; + + "aws-strands": async () => ({ + // Different URL pattern (hyphens) and one has debug:true, so not using mapAgents + ...mapAgents( + (path) => new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/${path}/` }), + { + agentic_chat: "agentic-chat", + backend_tool_rendering: "backend-tool-rendering", + agentic_generative_ui: "agentic-generative-ui", + shared_state: "shared-state", + } + ), + human_in_the_loop: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/human-in-the-loop`, debug: true }), + }), +} satisfies AgentsMap; diff --git a/apps/dojo/src/app/[integrationId]/feature/layout-client.tsx b/apps/dojo/src/app/[integrationId]/feature/layout-client.tsx new file mode 100644 index 000000000..653e1b93d --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/layout-client.tsx @@ -0,0 +1,68 @@ +'use client'; + +import React, { useMemo } from "react"; +import filesJSON from '../../../files.json' +import Readme from "@/components/readme/readme"; +import CodeViewer from "@/components/code-viewer/code-viewer"; +import { useURLParams } from "@/contexts/url-params-context"; +import { cn } from "@/lib/utils"; +import { Feature } from "@/types/integration"; + +type FileItem = { + name: string; + content: string; + language: string; + type: string; +}; + +type FilesJsonType = Record; + +interface Props { + integrationId: string; + featureId: Feature; + children: React.ReactNode; +} + +export default function FeatureLayoutClient({ children, integrationId, featureId }: Props) { + const { sidebarHidden } = useURLParams(); + const { view } = useURLParams(); + + const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`] || []; + + const readme = files.find((file) => file?.name?.includes(".mdx")) || null; + const codeFiles = files.filter( + (file) => file && Object.keys(file).length > 0 && !file.name?.includes(".mdx"), + ); + + const content = useMemo(() => { + switch (view) { + case "code": + return ( + + ) + case "readme": + return ( + + ) + default: + return ( +
{children}
+ ) + } + }, [children, codeFiles, readme, view]) + + return ( +
+
+ {content} +
+
+ ); +} + diff --git a/apps/dojo/src/app/[integrationId]/feature/layout.tsx b/apps/dojo/src/app/[integrationId]/feature/layout.tsx index 9a1000099..6070a86ad 100644 --- a/apps/dojo/src/app/[integrationId]/feature/layout.tsx +++ b/apps/dojo/src/app/[integrationId]/feature/layout.tsx @@ -1,75 +1,38 @@ -'use client'; +import { headers } from "next/headers"; +import { notFound } from "next/navigation"; +import { Feature } from "@/types/integration"; +import FeatureLayoutClient from "./layout-client"; -import React, { useMemo } from "react"; -import { usePathname } from "next/navigation"; -import filesJSON from '../../../files.json' -import Readme from "@/components/readme/readme"; -import CodeViewer from "@/components/code-viewer/code-viewer"; -import { useURLParams } from "@/contexts/url-params-context"; -import { cn } from "@/lib/utils"; - -type FileItem = { - name: string; - content: string; - language: string; - type: string; -}; - -type FilesJsonType = Record; +// Force dynamic rendering to ensure proper 404 handling +export const dynamic = "force-dynamic"; interface Props { params: Promise<{ integrationId: string; }>; - children: React.ReactNode + children: React.ReactNode; } -export default function FeatureLayout({ children, params }: Props) { - const { sidebarHidden } = useURLParams(); - const { integrationId } = React.use(params); - const pathname = usePathname(); - const { view } = useURLParams(); +export default async function FeatureLayout({ children, params }: Props) { + const { integrationId } = await params; - // Extract featureId from pathname: /[integrationId]/feature/[featureId] - const pathParts = pathname.split('/'); - const featureId = pathParts[pathParts.length - 1]; // Last segment is the featureId + // Get headers set by middleware + const headersList = await headers(); + const pathname = headersList.get("x-pathname") || ""; + const notFoundType = headersList.get("x-not-found"); - const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`] || []; + // If middleware flagged this as not found, trigger 404 + if (notFoundType) { + notFound(); + } - const readme = files.find((file) => file?.name?.includes(".mdx")) || null; - const codeFiles = files.filter( - (file) => file && Object.keys(file).length > 0 && !file.name?.includes(".mdx"), - ); - - - const content = useMemo(() => { - switch (view) { - case "code": - return ( - - ) - case "readme": - return ( - - ) - default: - return ( -
{children}
- ) - } - }, [children, codeFiles, readme, view]) + // Extract featureId from pathname: /[integrationId]/feature/[featureId] + const pathParts = pathname.split("/"); + const featureId = pathParts[pathParts.length - 1] as Feature; return ( -
-
- {content} -
-
+ + {children} + ); } diff --git a/apps/dojo/src/app/[integrationId]/feature/not-found.tsx b/apps/dojo/src/app/[integrationId]/feature/not-found.tsx new file mode 100644 index 000000000..16bac6d5e --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/not-found.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; + +export default function FeatureNotFound() { + return ( +
+

Feature Not Found

+

+ This feature is not available for the selected integration. +

+ + Back to Home + +
+ ); +} + diff --git a/apps/dojo/src/app/[integrationId]/page.tsx b/apps/dojo/src/app/[integrationId]/page.tsx index e216e5a63..634a10d4f 100644 --- a/apps/dojo/src/app/[integrationId]/page.tsx +++ b/apps/dojo/src/app/[integrationId]/page.tsx @@ -11,6 +11,9 @@ export async function generateStaticParams() { })); } +// Return 404 for any params not in generateStaticParams +export const dynamicParams = false; + interface IntegrationPageProps { params: Promise<{ integrationId: string; diff --git a/apps/dojo/src/app/api/copilotkit/[integrationId]/route.ts b/apps/dojo/src/app/api/copilotkit/[integrationId]/route.ts index a1c04b856..bce11890b 100644 --- a/apps/dojo/src/app/api/copilotkit/[integrationId]/route.ts +++ b/apps/dojo/src/app/api/copilotkit/[integrationId]/route.ts @@ -6,16 +6,17 @@ import { import { NextRequest } from "next/server"; import { agentsIntegrations } from "@/agents"; +import { IntegrationId } from "@/menu"; export async function POST(request: NextRequest) { - const integrationId = request.url.split("/").pop(); + const integrationId = request.url.split("/").pop() as IntegrationId; - const integration = agentsIntegrations.find((i) => i.id === integrationId); - if (!integration) { + const getAgents = agentsIntegrations[integrationId]; + if (!getAgents) { return new Response("Integration not found", { status: 404 }); } - const agents = await integration.agents(); + const agents = await getAgents(); const runtime = new CopilotRuntime({ // @ts-ignore for now agents, diff --git a/apps/dojo/src/app/not-found-page/page.tsx b/apps/dojo/src/app/not-found-page/page.tsx new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/apps/dojo/src/app/not-found-page/page.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 5b628beaf..defb34ddc 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -1,6 +1,16 @@ -import { MenuIntegrationConfig } from "./types/integration"; +import type { IntegrationFeatures, MenuIntegrationConfig } from "./types/integration"; -export const menuIntegrations: MenuIntegrationConfig[] = [ +/** + * Integration configuration - SINGLE SOURCE OF TRUTH + * + * This file defines all integrations and their available features. + * Used by: + * - UI menu components + * - middleware.ts (for route validation) + * - agents.ts validates agent keys against these features + */ + +export const menuIntegrations = [ { id: "langgraph", name: "LangGraph (Python)", @@ -35,7 +45,7 @@ export const menuIntegrations: MenuIntegrationConfig[] = [ name: "LangGraph (Typescript)", features: [ "agentic_chat", - "backend_tool_rendering", + // "backend_tool_rendering", "human_in_the_loop", "agentic_generative_ui", "predictive_state_updates", @@ -162,7 +172,7 @@ export const menuIntegrations: MenuIntegrationConfig[] = [ name: "CrewAI", features: [ "agentic_chat", - "backend_tool_rendering", + // "backend_tool_rendering", "human_in_the_loop", "agentic_generative_ui", "predictive_state_updates", @@ -198,7 +208,7 @@ export const menuIntegrations: MenuIntegrationConfig[] = [ "agentic_chat", "backend_tool_rendering", "human_in_the_loop", - "agentic_chat_reasoning", + // "agentic_chat_reasoning", "agentic_generative_ui", "predictive_state_updates", "shared_state", @@ -221,4 +231,27 @@ export const menuIntegrations: MenuIntegrationConfig[] = [ "human_in_the_loop", ], }, -]; +] as const satisfies readonly MenuIntegrationConfig[]; + +/** Type representing all valid integration IDs */ +export type IntegrationId = (typeof menuIntegrations)[number]["id"]; + +/** Type to get features for a specific integration ID */ +export type FeaturesFor = IntegrationFeatures< + typeof menuIntegrations, + Id +>; + +// Helper functions for route validation +export function isIntegrationValid(integrationId: string): boolean { + return menuIntegrations.some((i) => i.id === integrationId); +} + +export function isFeatureAvailable(integrationId: string, featureId: string): boolean { + const integration = menuIntegrations.find((i) => i.id === integrationId); + return (integration?.features as readonly string[])?.includes(featureId) ?? false; +} + +export function getIntegration(integrationId: string): MenuIntegrationConfig | undefined { + return menuIntegrations.find((i) => i.id === integrationId); +} diff --git a/apps/dojo/src/middleware.ts b/apps/dojo/src/middleware.ts new file mode 100644 index 000000000..59b668609 --- /dev/null +++ b/apps/dojo/src/middleware.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { isIntegrationValid, isFeatureAvailable } from "./menu"; + +export function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-pathname", pathname); + + // Check for feature routes: /[integrationId]/feature/[featureId] + const featureMatch = pathname.match(/^\/([^/]+)\/feature\/([^/]+)\/?$/); + + if (featureMatch) { + const [, integrationId, featureId] = featureMatch; + + // Check if integration exists + if (!isIntegrationValid(integrationId)) { + requestHeaders.set("x-not-found", "integration"); + } + // Check if feature is available for this integration + else if (!isFeatureAvailable(integrationId, featureId)) { + requestHeaders.set("x-not-found", "feature"); + } + } + + // Check for integration routes: /[integrationId] (but not /[integrationId]/feature/...) + const integrationMatch = pathname.match(/^\/([^/]+)\/?$/); + + if (integrationMatch) { + const [, integrationId] = integrationMatch; + + // Skip the root path + if (integrationId && integrationId !== "") { + if (!isIntegrationValid(integrationId)) { + requestHeaders.set("x-not-found", "integration"); + } + } + } + + return NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); +} + +export const config = { + matcher: [ + // Match all paths except static files and api routes + "/((?!api|_next/static|_next/image|favicon.ico|images).*)", + ], +}; + diff --git a/apps/dojo/src/types/integration.ts b/apps/dojo/src/types/integration.ts index 182933a6f..bd708cda3 100644 --- a/apps/dojo/src/types/integration.ts +++ b/apps/dojo/src/types/integration.ts @@ -1,5 +1,3 @@ -import { AbstractAgent } from "@ag-ui/client"; - export type Feature = | "agentic_chat" | "agentic_generative_ui" @@ -19,7 +17,10 @@ export interface MenuIntegrationConfig { features: Feature[]; } -export interface AgentIntegrationConfig { - id: string; - agents: () => Promise>>; -} +/** + * Helper type to extract features for a specific integration from menu config + */ +export type IntegrationFeatures< + T extends readonly MenuIntegrationConfig[], + Id extends string +> = Extract["features"][number]; diff --git a/integrations/mastra/typescript/src/mastra.ts b/integrations/mastra/typescript/src/mastra.ts index 27cbe4425..c199e708d 100644 --- a/integrations/mastra/typescript/src/mastra.ts +++ b/integrations/mastra/typescript/src/mastra.ts @@ -381,15 +381,15 @@ export class MastraAgent extends AbstractAgent { } } - static async getRemoteAgents( - options: GetRemoteAgentsOptions, - ): Promise> { + static async getRemoteAgents( + options: GetRemoteAgentsOptions, + ): Promise> { return getRemoteAgents(options); } - static getLocalAgents( - options: GetLocalAgentsOptions, - ): Record { + static getLocalAgents( + options: GetLocalAgentsOptions, + ): Record { return getLocalAgents(options); } diff --git a/integrations/mastra/typescript/src/utils.ts b/integrations/mastra/typescript/src/utils.ts index e775d353f..3639e3e95 100644 --- a/integrations/mastra/typescript/src/utils.ts +++ b/integrations/mastra/typescript/src/utils.ts @@ -86,22 +86,24 @@ export function convertAGUIMessagesToMastra(messages: Message[]): CoreMessage[] return result; } -export interface GetRemoteAgentsOptions { +export interface GetRemoteAgentsOptions { mastraClient: MastraClient; resourceId?: string; + /** Expected agent keys - used for type inference only */ + keys?: readonly K[]; } -export async function getRemoteAgents({ +export async function getRemoteAgents({ mastraClient, resourceId, -}: GetRemoteAgentsOptions): Promise> { +}: GetRemoteAgentsOptions): Promise> { const agents = await mastraClient.getAgents(); return Object.entries(agents).reduce( (acc, [agentId]) => { const agent = mastraClient.getAgent(agentId); - acc[agentId] = new MastraAgent({ + acc[agentId as K] = new MastraAgent({ agentId, agent, resourceId, @@ -109,26 +111,28 @@ export async function getRemoteAgents({ return acc; }, - {} as Record, + {} as Record, ); } -export interface GetLocalAgentsOptions { +export interface GetLocalAgentsOptions { mastra: Mastra; resourceId?: string; runtimeContext?: RuntimeContext; + /** Expected agent keys - used for type inference only */ + keys?: readonly K[]; } -export function getLocalAgents({ +export function getLocalAgents({ mastra, resourceId, runtimeContext, -}: GetLocalAgentsOptions): Record { +}: GetLocalAgentsOptions): Record { const agents = mastra.getAgents() || {}; const agentAGUI = Object.entries(agents).reduce( (acc, [agentId, agent]) => { - acc[agentId] = new MastraAgent({ + acc[agentId as K] = new MastraAgent({ agentId, agent, resourceId, @@ -136,7 +140,7 @@ export function getLocalAgents({ }); return acc; }, - {} as Record, + {} as Record, ); return agentAGUI;