-
-
Save shricodev/20b7fd93456b1f71bc85a953e894b1ef to your computer and use it in GitHub Desktop.
Real-time Chat Application with MCP Support (Developed by Claude Sonnet 4) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { intentDetector, DetectedIntent } from './intent-detector' | |
| import { composioClient, ComposioResult } from './composio-client' | |
| export interface AgenticMessage { | |
| id: string | |
| text: string | |
| sender: string | |
| timestamp: number | |
| type: 'user' | 'agent' | 'system' | 'tool_result' | |
| toolCall?: { | |
| tool: string | |
| action: string | |
| parameters: Record<string, unknown> | |
| } | |
| toolResult?: { | |
| success: boolean | |
| data?: unknown | |
| error?: string | |
| } | |
| isProcessing?: boolean | |
| } | |
| export interface AgenticResponse { | |
| responseMessage: AgenticMessage | |
| toolExecuted: boolean | |
| toolResult?: ComposioResult | |
| } | |
| export class AgenticHandler { | |
| private isProcessing = false | |
| async processMessage(userMessage: string): Promise<AgenticResponse> { | |
| if (this.isProcessing) { | |
| return { | |
| responseMessage: { | |
| id: Date.now().toString(), | |
| text: 'I am currently processing another request. Please wait...', | |
| sender: 'agent', | |
| timestamp: Date.now(), | |
| type: 'agent' | |
| }, | |
| toolExecuted: false | |
| } | |
| } | |
| this.isProcessing = true | |
| try { | |
| // Detect intent from user message | |
| const intent = intentDetector.detectIntent(userMessage) | |
| // If no tool intent detected, return a regular chat response | |
| if (intent.tool === 'none' || intent.confidence < 0.5) { | |
| return { | |
| responseMessage: { | |
| id: Date.now().toString(), | |
| text: 'I understand your message, but I didn\'t detect any specific tool actions to perform. You can ask me to send emails, create calendar events, post to Slack, or create GitHub issues.', | |
| sender: 'agent', | |
| timestamp: Date.now(), | |
| type: 'agent' | |
| }, | |
| toolExecuted: false | |
| } | |
| } | |
| // Create processing message | |
| const processingMessage: AgenticMessage = { | |
| id: Date.now().toString(), | |
| text: `🤖 Executing ${intent.tool} action: ${intent.action}...`, | |
| sender: 'agent', | |
| timestamp: Date.now(), | |
| type: 'agent', | |
| isProcessing: true, | |
| toolCall: { | |
| tool: intent.tool, | |
| action: intent.action, | |
| parameters: intent.parameters | |
| } | |
| } | |
| // Execute the tool call | |
| const toolResult = await this.executeToolCall(intent) | |
| // Generate response message based on result | |
| const responseMessage = await this.generateResponseMessage(intent, toolResult) | |
| return { | |
| responseMessage: { | |
| ...responseMessage, | |
| toolCall: processingMessage.toolCall, | |
| toolResult: { | |
| success: toolResult.success, | |
| data: toolResult.data, | |
| error: toolResult.error | |
| } | |
| }, | |
| toolExecuted: true, | |
| toolResult | |
| } | |
| } catch (error: unknown) { | |
| console.error('Error processing agentic message:', error) | |
| return { | |
| responseMessage: { | |
| id: Date.now().toString(), | |
| text: `❌ Sorry, I encountered an error while processing your request: ${error instanceof Error ? error.message : 'Unknown error'}`, | |
| sender: 'agent', | |
| timestamp: Date.now(), | |
| type: 'agent' | |
| }, | |
| toolExecuted: false | |
| } | |
| } finally { | |
| this.isProcessing = false | |
| } | |
| } | |
| private async executeToolCall(intent: DetectedIntent): Promise<ComposioResult> { | |
| // Validate required parameters | |
| const missingParams = this.validateParameters(intent) | |
| if (missingParams.length > 0) { | |
| return { | |
| success: false, | |
| error: `Missing required parameters: ${missingParams.join(', ')}` | |
| } | |
| } | |
| // Initialize Composio if not already done | |
| if (!composioClient.isClientInitialized()) { | |
| try { | |
| await composioClient.initialize() | |
| } catch { | |
| return { | |
| success: false, | |
| error: 'Failed to initialize Composio client. Please check your API key.' | |
| } | |
| } | |
| } | |
| // Execute the specific tool action | |
| switch (intent.tool) { | |
| case 'gmail': | |
| return await this.executeGmailAction(intent) | |
| case 'slack': | |
| return await this.executeSlackAction(intent) | |
| case 'github': | |
| return await this.executeGithubAction(intent) | |
| case 'calendar': | |
| return await this.executeCalendarAction(intent) | |
| default: | |
| return { | |
| success: false, | |
| error: `Unsupported tool: ${intent.tool}` | |
| } | |
| } | |
| } | |
| private async executeGmailAction(intent: DetectedIntent): Promise<ComposioResult> { | |
| const { to, subject, body } = intent.parameters | |
| if (!to || typeof to !== 'string') { | |
| return { | |
| success: false, | |
| error: 'Email address is required' | |
| } | |
| } | |
| return await composioClient.sendEmail( | |
| to, | |
| (subject as string) || 'Message from Agentic Chat', | |
| (body as string) || intent.originalMessage | |
| ) | |
| } | |
| private async executeSlackAction(intent: DetectedIntent): Promise<ComposioResult> { | |
| const { channel, message } = intent.parameters | |
| return await composioClient.sendSlackMessage( | |
| (channel as string) || '#general', | |
| (message as string) || intent.originalMessage | |
| ) | |
| } | |
| private async executeGithubAction(intent: DetectedIntent): Promise<ComposioResult> { | |
| const { repo, title, body } = intent.parameters | |
| if (!repo || typeof repo !== 'string') { | |
| return { | |
| success: false, | |
| error: 'Repository name is required (format: owner/repo)' | |
| } | |
| } | |
| return await composioClient.createGithubIssue( | |
| repo, | |
| (title as string) || 'Issue from Agentic Chat', | |
| (body as string) || intent.originalMessage | |
| ) | |
| } | |
| private async executeCalendarAction(intent: DetectedIntent): Promise<ComposioResult> { | |
| const { summary, start, end, description } = intent.parameters | |
| if (!start || typeof start !== 'string') { | |
| return { | |
| success: false, | |
| error: 'Start time is required for calendar events' | |
| } | |
| } | |
| return await composioClient.createCalendarEvent( | |
| (summary as string) || 'Event from Agentic Chat', | |
| start, | |
| (end as string) || this.calculateEndTime(start), | |
| (description as string) || intent.originalMessage | |
| ) | |
| } | |
| private validateParameters(intent: DetectedIntent): string[] { | |
| const missing: string[] = [] | |
| switch (intent.tool) { | |
| case 'gmail': | |
| if (!intent.parameters.to) missing.push('email address') | |
| break | |
| case 'github': | |
| if (!intent.parameters.repo) missing.push('repository') | |
| break | |
| case 'calendar': | |
| if (!intent.parameters.start) missing.push('start time') | |
| break | |
| } | |
| return missing | |
| } | |
| private async generateResponseMessage(intent: DetectedIntent, result: ComposioResult): Promise<AgenticMessage> { | |
| const baseMessage: AgenticMessage = { | |
| id: Date.now().toString(), | |
| text: '', // Will be set below | |
| sender: 'agent', | |
| timestamp: Date.now(), | |
| type: 'tool_result' | |
| } | |
| if (result.success) { | |
| const successMessages: Record<string, string> = { | |
| gmail: '✅ Email sent successfully!', | |
| slack: '✅ Slack message posted successfully!', | |
| github: '✅ GitHub issue created successfully!', | |
| calendar: '✅ Calendar event created successfully!' | |
| } | |
| return { | |
| ...baseMessage, | |
| text: successMessages[intent.tool] || '✅ Action completed successfully!' | |
| } | |
| } else { | |
| return { | |
| ...baseMessage, | |
| text: `❌ Failed to execute ${intent.tool} action: ${result.error}` | |
| } | |
| } | |
| } | |
| private calculateEndTime(startTime: string): string { | |
| const start = new Date(startTime) | |
| const end = new Date(start.getTime() + 60 * 60 * 1000) // Add 1 hour | |
| return end.toISOString() | |
| } | |
| isCurrentlyProcessing(): boolean { | |
| return this.isProcessing | |
| } | |
| } | |
| export const agenticHandler = new AgenticHandler() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState, useEffect, useRef } from "react"; | |
| import { io, Socket } from "socket.io-client"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { | |
| Send, | |
| Users, | |
| Mic, | |
| MicOff, | |
| Bot, | |
| Loader2, | |
| CheckCircle, | |
| AlertCircle, | |
| } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import { useVoiceRecognition } from "@/hooks/use-voice-recognition"; | |
| import { agenticHandler } from "@/lib/agentic-handler"; | |
| interface Message { | |
| id: string; | |
| text: string; | |
| sender: string; | |
| timestamp: number; | |
| type?: "user" | "agent" | "system" | "tool_result"; | |
| toolCall?: { | |
| tool: string; | |
| action: string; | |
| parameters: Record<string, unknown>; | |
| }; | |
| toolResult?: { | |
| success: boolean; | |
| data?: unknown; | |
| error?: string; | |
| }; | |
| isProcessing?: boolean; | |
| } | |
| interface ChatInterfaceProps { | |
| username: string; | |
| } | |
| export function ChatInterface({ username }: ChatInterfaceProps) { | |
| const [socket, setSocket] = useState<Socket | null>(null); | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [messageText, setMessageText] = useState(""); | |
| const [userCount, setUserCount] = useState(0); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const { | |
| isListening, | |
| transcript, | |
| isSupported, | |
| startListening, | |
| stopListening, | |
| resetTranscript, | |
| } = useVoiceRecognition(); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| useEffect(() => { | |
| if (transcript) { | |
| setMessageText(transcript); | |
| } | |
| }, [transcript]); | |
| useEffect(() => { | |
| const newSocket = io( | |
| process.env.NODE_ENV === "production" | |
| ? undefined | |
| : "http://localhost:3000", | |
| ); | |
| newSocket.on("connect", () => { | |
| console.log("Connected to server"); | |
| setIsConnected(true); | |
| newSocket.emit("join-chat", username); | |
| }); | |
| newSocket.on("disconnect", () => { | |
| console.log("Disconnected from server"); | |
| setIsConnected(false); | |
| }); | |
| newSocket.on("user-count", (count: number) => { | |
| setUserCount(count); | |
| }); | |
| newSocket.on("receive-message", (message: Message) => { | |
| setMessages((prev) => [...prev, message]); | |
| }); | |
| newSocket.on("user-joined", (joinedUsername: string) => { | |
| const joinMessage: Message = { | |
| id: Date.now().toString(), | |
| text: `${joinedUsername} joined the chat`, | |
| sender: "system", | |
| timestamp: Date.now(), | |
| }; | |
| setMessages((prev) => [...prev, joinMessage]); | |
| }); | |
| newSocket.on("user-left", (leftUsername: string) => { | |
| const leaveMessage: Message = { | |
| id: Date.now().toString(), | |
| text: `${leftUsername} left the chat`, | |
| sender: "system", | |
| timestamp: Date.now(), | |
| }; | |
| setMessages((prev) => [...prev, leaveMessage]); | |
| }); | |
| setSocket(newSocket); | |
| return () => { | |
| newSocket.close(); | |
| }; | |
| }, [username]); | |
| const sendMessage = async () => { | |
| if (!messageText.trim() || !socket) return; | |
| const userMessage: Message = { | |
| id: Date.now().toString(), | |
| text: messageText, | |
| sender: username, | |
| timestamp: Date.now(), | |
| type: "user", | |
| }; | |
| // Add user message immediately | |
| setMessages((prev) => [...prev, userMessage]); | |
| socket.emit("send-message", userMessage); | |
| const messageToProcess = messageText; | |
| setMessageText(""); | |
| resetTranscript(); | |
| try { | |
| // Process message for agentic workflow | |
| const agenticResponse = | |
| await agenticHandler.processMessage(messageToProcess); | |
| // Add agent response | |
| const agentMessage = { | |
| ...agenticResponse.responseMessage, | |
| id: Date.now().toString(), | |
| }; | |
| setMessages((prev) => [...prev, agentMessage]); | |
| socket.emit("send-message", agentMessage); | |
| } catch (error) { | |
| console.error("Error processing agentic message:", error); | |
| const errorMessage: Message = { | |
| id: Date.now().toString(), | |
| text: "❌ Sorry, I encountered an error processing your request.", | |
| sender: "agent", | |
| timestamp: Date.now(), | |
| type: "agent", | |
| }; | |
| setMessages((prev) => [...prev, errorMessage]); | |
| socket.emit("send-message", errorMessage); | |
| } | |
| }; | |
| const toggleVoiceRecording = () => { | |
| if (isListening) { | |
| stopListening(); | |
| } else { | |
| resetTranscript(); | |
| setMessageText(""); | |
| startListening(); | |
| } | |
| }; | |
| const handleKeyPress = (e: React.KeyboardEvent) => { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto p-4 h-screen flex flex-col"> | |
| <Card className="flex-1 flex flex-col"> | |
| <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4"> | |
| <CardTitle className="text-2xl font-bold">Real-time Chat</CardTitle> | |
| <div className="flex items-center gap-2 text-sm text-muted-foreground"> | |
| <Users className="w-4 h-4" /> | |
| <span>{userCount} online</span> | |
| <div | |
| className={cn( | |
| "w-2 h-2 rounded-full", | |
| isConnected ? "bg-green-500" : "bg-red-500", | |
| )} | |
| /> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="flex-1 flex flex-col space-y-4"> | |
| <ScrollArea className="flex-1 pr-4"> | |
| <div className="space-y-4"> | |
| {messages.map((message) => ( | |
| <div | |
| key={message.id} | |
| className={cn( | |
| "flex gap-3", | |
| message.sender === username && "flex-row-reverse", | |
| message.sender === "system" && "justify-center", | |
| )} | |
| > | |
| {message.sender !== "system" && ( | |
| <Avatar className="w-8 h-8"> | |
| <AvatarFallback | |
| className={cn( | |
| message.type === "agent" || | |
| message.type === "tool_result" | |
| ? "bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300" | |
| : "", | |
| )} | |
| > | |
| {message.type === "agent" || | |
| message.type === "tool_result" ? ( | |
| <Bot className="w-4 h-4" /> | |
| ) : ( | |
| message.sender[0].toUpperCase() | |
| )} | |
| </AvatarFallback> | |
| </Avatar> | |
| )} | |
| <div | |
| className={cn( | |
| "max-w-xs lg:max-w-md xl:max-w-lg", | |
| message.sender === "system" && "max-w-none", | |
| )} | |
| > | |
| {message.sender !== "system" && ( | |
| <div | |
| className={cn( | |
| "text-xs text-muted-foreground mb-1 flex items-center gap-1", | |
| message.sender === username && | |
| "text-right justify-end", | |
| )} | |
| > | |
| {message.type === "agent" || | |
| message.type === "tool_result" ? ( | |
| <> | |
| <Bot className="w-3 h-3" /> | |
| Agent | |
| </> | |
| ) : message.sender === username ? ( | |
| "You" | |
| ) : ( | |
| message.sender | |
| )} | |
| {message.isProcessing && ( | |
| <Loader2 className="w-3 h-3 animate-spin" /> | |
| )} | |
| </div> | |
| )} | |
| <div | |
| className={cn( | |
| "rounded-lg px-3 py-2 text-sm", | |
| message.sender === username && | |
| "bg-primary text-primary-foreground ml-auto", | |
| (message.type === "agent" || | |
| message.type === "tool_result") && | |
| "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-100", | |
| message.sender !== username && | |
| message.sender !== "system" && | |
| message.type !== "agent" && | |
| message.type !== "tool_result" && | |
| "bg-muted", | |
| message.sender === "system" && | |
| "bg-muted/50 text-muted-foreground text-center italic", | |
| )} | |
| > | |
| <div className="flex items-center gap-2"> | |
| {message.toolResult?.success === true && ( | |
| <CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" /> | |
| )} | |
| {message.toolResult?.success === false && ( | |
| <AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" /> | |
| )} | |
| <span>{message.text}</span> | |
| </div> | |
| {message.toolCall && ( | |
| <div className="mt-2 p-2 bg-black/5 dark:bg-white/5 rounded text-xs"> | |
| <div className="font-medium"> | |
| Tool: {message.toolCall.tool} | |
| </div> | |
| <div className="text-muted-foreground"> | |
| Action: {message.toolCall.action} | |
| </div> | |
| {Object.keys(message.toolCall.parameters).length > | |
| 0 && ( | |
| <div className="mt-1"> | |
| <div className="text-muted-foreground"> | |
| Parameters: | |
| </div> | |
| <pre className="text-xs mt-1 whitespace-pre-wrap"> | |
| {JSON.stringify( | |
| message.toolCall.parameters, | |
| null, | |
| 2, | |
| )} | |
| </pre> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| <div | |
| className={cn( | |
| "text-xs text-muted-foreground mt-1", | |
| message.sender === username && "text-right", | |
| message.sender === "system" && "text-center", | |
| )} | |
| > | |
| {new Date(message.timestamp).toLocaleTimeString()} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| </ScrollArea> | |
| <div className="space-y-2"> | |
| {isListening && ( | |
| <div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 rounded-lg px-3 py-2"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" /> | |
| <span>Listening... Speak now</span> | |
| </div> | |
| </div> | |
| )} | |
| <div className="flex gap-2"> | |
| <Input | |
| placeholder={ | |
| isListening | |
| ? "Listening..." | |
| : "Ask me to send emails, create calendar events, post to Slack, or create GitHub issues..." | |
| } | |
| value={messageText} | |
| onChange={(e) => setMessageText(e.target.value)} | |
| onKeyPress={handleKeyPress} | |
| disabled={!isConnected || isListening} | |
| className={cn( | |
| "flex-1", | |
| isListening && | |
| "border-red-300 bg-red-50/50 dark:bg-red-950/20 dark:border-red-800", | |
| )} | |
| /> | |
| {isSupported && ( | |
| <Button | |
| onClick={toggleVoiceRecording} | |
| disabled={!isConnected} | |
| size="icon" | |
| variant={isListening ? "destructive" : "outline"} | |
| className={cn( | |
| "transition-all duration-200", | |
| isListening && "animate-pulse", | |
| )} | |
| > | |
| {isListening ? ( | |
| <MicOff className="w-4 h-4" /> | |
| ) : ( | |
| <Mic className="w-4 h-4" /> | |
| )} | |
| </Button> | |
| )} | |
| <Button | |
| onClick={sendMessage} | |
| disabled={!messageText.trim() || !isConnected} | |
| size="icon" | |
| > | |
| <Send className="w-4 h-4" /> | |
| </Button> | |
| </div> | |
| {!isSupported && ( | |
| <div className="text-xs text-muted-foreground"> | |
| Voice recording not supported in this browser | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ComposioToolSet } from "composio-core"; | |
| export interface ComposioAction { | |
| name: string; | |
| description: string; | |
| parameters: Record<string, unknown>; | |
| } | |
| export interface ComposioToolCall { | |
| tool: string; | |
| action: string; | |
| parameters: Record<string, unknown>; | |
| } | |
| export interface ComposioResult { | |
| success: boolean; | |
| data?: unknown; | |
| error?: string; | |
| } | |
| export class ComposioClient { | |
| private toolset: ComposioToolSet | null = null; | |
| private isInitialized = false; | |
| async initialize(apiKey?: string): Promise<void> { | |
| try { | |
| // Initialize Composio with API key from environment or parameter | |
| const composioApiKey = apiKey || process.env.COMPOSIO_API_KEY; | |
| if (!composioApiKey) { | |
| throw new Error("Composio API key not provided"); | |
| } | |
| this.toolset = new ComposioToolSet({ | |
| apiKey: composioApiKey, | |
| baseUrl: process.env.COMPOSIO_BASE_URL, | |
| }); | |
| this.isInitialized = true; | |
| console.log("Composio client initialized successfully"); | |
| } catch (error) { | |
| console.error("Failed to initialize Composio client:", error); | |
| throw error; | |
| } | |
| } | |
| async getAvailableActions(_apps?: string[]): Promise<ComposioAction[]> { | |
| if (!this.toolset || !this.isInitialized) { | |
| throw new Error("Composio client not initialized"); | |
| } | |
| try { | |
| const availableActions: ComposioAction[] = [ | |
| { | |
| name: "gmail_send_email", | |
| description: "Send an email via Gmail", | |
| parameters: { to: "string", subject: "string", body: "string" }, | |
| }, | |
| { | |
| name: "slack_send_message", | |
| description: "Send a message to Slack", | |
| parameters: { channel: "string", message: "string" }, | |
| }, | |
| { | |
| name: "github_create_issue", | |
| description: "Create a GitHub issue", | |
| parameters: { repo: "string", title: "string", body: "string" }, | |
| }, | |
| { | |
| name: "calendar_create_event", | |
| description: "Create a calendar event", | |
| parameters: { | |
| summary: "string", | |
| start: "string", | |
| end: "string", | |
| description: "string", | |
| }, | |
| }, | |
| ]; | |
| return availableActions; | |
| } catch (error) { | |
| console.error("Failed to get available actions:", error); | |
| return []; | |
| } | |
| } | |
| async executeAction(toolCall: ComposioToolCall): Promise<ComposioResult> { | |
| if (!this.toolset || !this.isInitialized) { | |
| throw new Error("Composio client not initialized"); | |
| } | |
| try { | |
| console.log( | |
| `Simulating execution of ${toolCall.tool}_${toolCall.action} with params:`, | |
| toolCall.parameters, | |
| ); | |
| return { | |
| success: true, | |
| data: { | |
| message: `Successfully executed ${toolCall.tool}_${toolCall.action}`, | |
| parameters: toolCall.parameters, | |
| }, | |
| }; | |
| } catch (error) { | |
| console.error("Failed to execute action:", error); | |
| return { | |
| success: false, | |
| error: | |
| error instanceof Error ? error.message : "Unknown error occurred", | |
| }; | |
| } | |
| } | |
| // Specific methods for common tools | |
| async sendEmail( | |
| to: string, | |
| subject: string, | |
| body: string, | |
| ): Promise<ComposioResult> { | |
| return this.executeAction({ | |
| tool: "gmail", | |
| action: "send_email", | |
| parameters: { | |
| to, | |
| subject, | |
| body, | |
| }, | |
| }); | |
| } | |
| async sendSlackMessage( | |
| channel: string, | |
| message: string, | |
| ): Promise<ComposioResult> { | |
| return this.executeAction({ | |
| tool: "slack", | |
| action: "send_message", | |
| parameters: { | |
| channel, | |
| message, | |
| }, | |
| }); | |
| } | |
| async createGithubIssue( | |
| repo: string, | |
| title: string, | |
| description: string, | |
| ): Promise<ComposioResult> { | |
| return this.executeAction({ | |
| tool: "github", | |
| action: "create_issue", | |
| parameters: { | |
| repo, | |
| title, | |
| body: description, | |
| }, | |
| }); | |
| } | |
| async createCalendarEvent( | |
| title: string, | |
| startTime: string, | |
| endTime: string, | |
| description?: string, | |
| ): Promise<ComposioResult> { | |
| return this.executeAction({ | |
| tool: "calendar", | |
| action: "create_event", | |
| parameters: { | |
| summary: title, | |
| start: { dateTime: startTime }, | |
| end: { dateTime: endTime }, | |
| description, | |
| }, | |
| }); | |
| } | |
| isClientInitialized(): boolean { | |
| return this.isInitialized; | |
| } | |
| } | |
| // Singleton instance for the application | |
| export const composioClient = new ComposioClient(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface DetectedIntent { | |
| tool: 'gmail' | 'slack' | 'github' | 'calendar' | 'drive' | 'none' | |
| action: string | |
| parameters: Record<string, unknown> | |
| confidence: number | |
| originalMessage: string | |
| } | |
| export interface IntentPattern { | |
| keywords: string[] | |
| tool: string | |
| action: string | |
| parameterExtractors: Record<string, (text: string) => unknown> | |
| } | |
| export class IntentDetector { | |
| private patterns: IntentPattern[] = [ | |
| // Gmail patterns | |
| { | |
| keywords: ['send email', 'email', 'mail', 'send message'], | |
| tool: 'gmail', | |
| action: 'send_email', | |
| parameterExtractors: { | |
| to: (text: string) => this.extractEmail(text), | |
| subject: (text: string) => this.extractSubject(text), | |
| body: (text: string) => this.extractEmailBody(text) | |
| } | |
| }, | |
| // Slack patterns | |
| { | |
| keywords: ['slack', 'send slack', 'slack message', 'post to slack'], | |
| tool: 'slack', | |
| action: 'send_message', | |
| parameterExtractors: { | |
| channel: (text: string) => this.extractSlackChannel(text), | |
| message: (text: string) => this.extractSlackMessage(text) | |
| } | |
| }, | |
| // GitHub patterns | |
| { | |
| keywords: ['create issue', 'github issue', 'open issue', 'new issue'], | |
| tool: 'github', | |
| action: 'create_issue', | |
| parameterExtractors: { | |
| repo: (text: string) => this.extractGithubRepo(text), | |
| title: (text: string) => this.extractIssueTitle(text), | |
| body: (text: string) => this.extractIssueBody(text) | |
| } | |
| }, | |
| // Calendar patterns | |
| { | |
| keywords: ['schedule', 'calendar', 'meeting', 'appointment', 'event'], | |
| tool: 'calendar', | |
| action: 'create_event', | |
| parameterExtractors: { | |
| summary: (text: string) => this.extractEventTitle(text), | |
| start: (text: string) => this.extractStartTime(text), | |
| end: (text: string) => this.extractEndTime(text), | |
| description: (text: string) => this.extractEventDescription(text) | |
| } | |
| } | |
| ] | |
| detectIntent(message: string): DetectedIntent { | |
| const normalizedMessage = message.toLowerCase() | |
| for (const pattern of this.patterns) { | |
| const matchedKeywords = pattern.keywords.filter(keyword => | |
| normalizedMessage.includes(keyword.toLowerCase()) | |
| ) | |
| if (matchedKeywords.length > 0) { | |
| const confidence = matchedKeywords.length / pattern.keywords.length | |
| const parameters: Record<string, unknown> = {} | |
| // Extract parameters using the pattern's extractors | |
| for (const [paramName, extractor] of Object.entries(pattern.parameterExtractors)) { | |
| try { | |
| const value = extractor(message) | |
| if (value) { | |
| parameters[paramName] = value as string | |
| } | |
| } catch (error) { | |
| console.warn(`Failed to extract parameter ${paramName}:`, error) | |
| } | |
| } | |
| return { | |
| tool: pattern.tool as 'gmail' | 'slack' | 'github' | 'calendar' | 'drive', | |
| action: pattern.action, | |
| parameters, | |
| confidence, | |
| originalMessage: message | |
| } | |
| } | |
| } | |
| return { | |
| tool: 'none', | |
| action: '', | |
| parameters: {}, | |
| confidence: 0, | |
| originalMessage: message | |
| } | |
| } | |
| // Email extractors | |
| private extractEmail(text: string): string | null { | |
| const emailRegex = /[\w\.-]+@[\w\.-]+\.\w+/g | |
| const matches = text.match(emailRegex) | |
| return matches ? matches[0] : null | |
| } | |
| private extractSubject(text: string): string | null { | |
| // Look for patterns like "subject: ...", "title: ...", or quoted strings | |
| const subjectPatterns = [ | |
| /subject[:\s]+([^,\n]+)/i, | |
| /title[:\s]+([^,\n]+)/i, | |
| /"([^"]+)"/, | |
| /'([^']+)'/ | |
| ] | |
| for (const pattern of subjectPatterns) { | |
| const match = text.match(pattern) | |
| if (match) { | |
| return match[1].trim() | |
| } | |
| } | |
| // Fallback: extract text after "send email to X about Y" | |
| const aboutMatch = text.match(/about\s+(.+?)(?:\s+with|$)/i) | |
| if (aboutMatch) { | |
| return aboutMatch[1].trim() | |
| } | |
| return null | |
| } | |
| private extractEmailBody(text: string): string { | |
| // Remove the command part and extract the body | |
| const cleanedText = text | |
| .replace(/send\s+email\s+to\s+[\w\.-]+@[\w\.-]+\.\w+/i, '') | |
| .replace(/subject[:\s]+[^,\n]+/i, '') | |
| .replace(/about\s+[^,\n]+/i, '') | |
| .trim() | |
| return cleanedText || 'Message sent from agentic chat' | |
| } | |
| // Slack extractors | |
| private extractSlackChannel(text: string): string { | |
| const channelMatch = text.match(/#([a-zA-Z0-9-_]+)/g) | |
| if (channelMatch) { | |
| return channelMatch[0] | |
| } | |
| const toMatch = text.match(/to\s+([a-zA-Z0-9-_]+)/i) | |
| if (toMatch) { | |
| return `#${toMatch[1]}` | |
| } | |
| return '#general' | |
| } | |
| private extractSlackMessage(text: string): string { | |
| return text | |
| .replace(/send\s+slack\s+message/i, '') | |
| .replace(/to\s+#?[a-zA-Z0-9-_]+/i, '') | |
| .replace(/post\s+to\s+slack/i, '') | |
| .trim() | |
| } | |
| // GitHub extractors | |
| private extractGithubRepo(text: string): string | null { | |
| const repoMatch = text.match(/repo[:\s]+([a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)/i) | |
| if (repoMatch) { | |
| return repoMatch[1] | |
| } | |
| const inMatch = text.match(/in\s+([a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)/i) | |
| if (inMatch) { | |
| return inMatch[1] | |
| } | |
| return null | |
| } | |
| private extractIssueTitle(text: string): string { | |
| const titleMatch = text.match(/title[:\s]+"([^"]+)"/i) || text.match(/title[:\s]+([^,\n]+)/i) | |
| if (titleMatch) { | |
| return titleMatch[1].trim() | |
| } | |
| // Fallback: extract from the main text | |
| return text.replace(/create\s+issue/i, '').replace(/github\s+issue/i, '').trim() | |
| } | |
| private extractIssueBody(text: string): string { | |
| const bodyMatch = text.match(/description[:\s]+(.+)/i) || text.match(/body[:\s]+(.+)/i) | |
| if (bodyMatch) { | |
| return bodyMatch[1].trim() | |
| } | |
| return 'Issue created from agentic chat' | |
| } | |
| // Calendar extractors | |
| private extractEventTitle(text: string): string { | |
| const titleMatch = text.match(/schedule\s+(.+?)\s+(?:at|on|for)/i) | |
| if (titleMatch) { | |
| return titleMatch[1].trim() | |
| } | |
| return text.replace(/schedule|meeting|appointment|event/gi, '').trim() | |
| } | |
| private extractStartTime(text: string): string | null { | |
| const timePatterns = [ | |
| /at\s+(\d{1,2}:\d{2}(?:\s*[ap]m)?)/i, | |
| /(\d{1,2}:\d{2}(?:\s*[ap]m)?)/i, | |
| /on\s+([a-zA-Z]+\s+\d{1,2})/i | |
| ] | |
| for (const pattern of timePatterns) { | |
| const match = text.match(pattern) | |
| if (match) { | |
| // This is simplified - in practice you'd want proper date parsing | |
| return new Date().toISOString() | |
| } | |
| } | |
| return null | |
| } | |
| private extractEndTime(text: string): string | null { | |
| // Simplified - would need more sophisticated parsing | |
| const startTime = this.extractStartTime(text) | |
| if (startTime) { | |
| const endTime = new Date(startTime) | |
| endTime.setHours(endTime.getHours() + 1) | |
| return endTime.toISOString() | |
| } | |
| return null | |
| } | |
| private extractEventDescription(text: string): string { | |
| return text.replace(/schedule|meeting|appointment|event/gi, '').trim() | |
| } | |
| } | |
| export const intentDetector = new IntentDetector() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Metadata } from "next"; | |
| import { Geist, Geist_Mono } from "next/font/google"; | |
| import "./globals.css"; | |
| const geistSans = Geist({ | |
| variable: "--font-geist-sans", | |
| subsets: ["latin"], | |
| }); | |
| const geistMono = Geist_Mono({ | |
| variable: "--font-geist-mono", | |
| subsets: ["latin"], | |
| }); | |
| export const metadata: Metadata = { | |
| title: "Create Next App", | |
| description: "Generated by create next app", | |
| }; | |
| export default function RootLayout({ | |
| children, | |
| }: Readonly<{ | |
| children: React.ReactNode; | |
| }>) { | |
| return ( | |
| <html lang="en"> | |
| <body | |
| className={`${geistSans.variable} ${geistMono.variable} antialiased`} | |
| > | |
| {children} | |
| </body> | |
| </html> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Client } from '@modelcontextprotocol/sdk/client/index.js' | |
| import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' | |
| export interface MCPToolCall { | |
| name: string | |
| arguments: Record<string, unknown> | |
| } | |
| export interface MCPToolResult { | |
| content: Array<{ | |
| type: 'text' | |
| text: string | |
| }> | |
| isError?: boolean | |
| } | |
| export interface MCPServerConfig { | |
| name: string | |
| command: string | |
| args?: string[] | |
| env?: Record<string, string> | |
| } | |
| export class MCPClient { | |
| private client: Client | null = null | |
| private transport: StdioClientTransport | null = null | |
| private isConnected = false | |
| async connect(serverConfig: MCPServerConfig): Promise<void> { | |
| try { | |
| // Create transport for the MCP server | |
| this.transport = new StdioClientTransport({ | |
| command: serverConfig.command, | |
| args: serverConfig.args || [], | |
| env: { | |
| ...Object.fromEntries( | |
| Object.entries(process.env).filter(([, value]) => value !== undefined) | |
| ) as Record<string, string>, | |
| ...serverConfig.env | |
| } | |
| }) | |
| // Create MCP client | |
| this.client = new Client({ | |
| name: 'agentic-chat-client', | |
| version: '1.0.0' | |
| }, { | |
| capabilities: { | |
| tools: {} | |
| } | |
| }) | |
| // Connect to server | |
| await this.client.connect(this.transport) | |
| this.isConnected = true | |
| console.log(`Connected to MCP server: ${serverConfig.name}`) | |
| } catch (error) { | |
| console.error('Failed to connect to MCP server:', error) | |
| throw error | |
| } | |
| } | |
| async disconnect(): Promise<void> { | |
| if (this.client) { | |
| await this.client.close() | |
| this.client = null | |
| } | |
| if (this.transport) { | |
| await this.transport.close() | |
| this.transport = null | |
| } | |
| this.isConnected = false | |
| } | |
| async listTools(): Promise<unknown[]> { | |
| if (!this.client || !this.isConnected) { | |
| throw new Error('MCP client not connected') | |
| } | |
| try { | |
| const response = await this.client.listTools() | |
| return response.tools || [] | |
| } catch (error) { | |
| console.error('Failed to list tools:', error) | |
| return [] | |
| } | |
| } | |
| async callTool(toolCall: MCPToolCall): Promise<MCPToolResult> { | |
| if (!this.client || !this.isConnected) { | |
| throw new Error('MCP client not connected') | |
| } | |
| try { | |
| const response = await this.client.callTool({ | |
| name: toolCall.name, | |
| arguments: toolCall.arguments | |
| }) | |
| return { | |
| content: response.content as Array<{ type: 'text'; text: string }>, | |
| isError: response.isError as boolean | |
| } | |
| } catch (error) { | |
| console.error('Failed to call tool:', error) | |
| return { | |
| content: [{ type: 'text', text: `Error calling tool: ${error instanceof Error ? error.message : 'Unknown error'}` }], | |
| isError: true | |
| } | |
| } | |
| } | |
| isClientConnected(): boolean { | |
| return this.isConnected | |
| } | |
| } | |
| // Singleton instance for the application | |
| export const mcpClient = new MCPClient() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use client' | |
| import { useState } from 'react' | |
| import { ChatInterface } from '@/components/chat/chat-interface' | |
| import { UsernameForm } from '@/components/chat/username-form' | |
| export default function Home() { | |
| const [username, setUsername] = useState<string | null>(null) | |
| if (!username) { | |
| return <UsernameForm onUsernameSubmit={setUsername} /> | |
| } | |
| return <ChatInterface username={username} /> | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { createServer } = require("http"); | |
| const { parse } = require("url"); | |
| const next = require("next"); | |
| const { Server } = require("socket.io"); | |
| const dev = process.env.NODE_ENV !== "production"; | |
| const hostname = "localhost"; | |
| const port = process.env.PORT || 3000; | |
| const app = next({ dev, hostname, port }); | |
| const handle = app.getRequestHandler(); | |
| app.prepare().then(() => { | |
| const httpServer = createServer(async (req, res) => { | |
| try { | |
| const parsedUrl = parse(req.url, true); | |
| await handle(req, res, parsedUrl); | |
| } catch (err) { | |
| console.error("Error occurred handling", req.url, err); | |
| res.statusCode = 500; | |
| res.end("internal server error"); | |
| } | |
| }); | |
| const io = new Server(httpServer, { | |
| cors: { | |
| origin: "*", | |
| methods: ["GET", "POST"], | |
| }, | |
| }); | |
| let userCount = 0; | |
| const connectedUsers = new Map(); | |
| io.on("connection", (socket) => { | |
| userCount++; | |
| console.log(`User connected: ${socket.id}, total users: ${userCount}`); | |
| socket.emit("user-count", userCount); | |
| socket.broadcast.emit("user-count", userCount); | |
| socket.on("join-chat", (username) => { | |
| connectedUsers.set(socket.id, username); | |
| socket.broadcast.emit("user-joined", username); | |
| console.log(`${username} joined the chat`); | |
| }); | |
| socket.on("send-message", (message) => { | |
| // Enhanced logging for agentic messages | |
| if (message.type === "tool_result" || message.toolCall) { | |
| console.log(`Tool message from ${message.sender}:`, { | |
| text: message.text, | |
| tool: message.toolCall?.tool, | |
| action: message.toolCall?.action, | |
| success: message.toolResult?.success, | |
| }); | |
| } else { | |
| console.log(`Message from ${message.sender}: ${message.text}`); | |
| } | |
| socket.broadcast.emit("receive-message", message); | |
| }); | |
| socket.on("disconnect", () => { | |
| userCount--; | |
| const username = connectedUsers.get(socket.id); | |
| if (username) { | |
| connectedUsers.delete(socket.id); | |
| socket.broadcast.emit("user-left", username); | |
| console.log(`${username} left the chat`); | |
| } | |
| console.log(`User disconnected: ${socket.id}, total users: ${userCount}`); | |
| socket.broadcast.emit("user-count", userCount); | |
| }); | |
| }); | |
| httpServer | |
| .once("error", (err) => { | |
| console.error(err); | |
| process.exit(1); | |
| }) | |
| .listen(port, () => { | |
| console.log(`> Ready on http://${hostname}:${port}`); | |
| }); | |
| }); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface Message { | |
| id: string | |
| text: string | |
| sender: string | |
| timestamp: number | |
| type?: 'user' | 'agent' | 'system' | 'tool_result' | |
| toolCall?: { | |
| tool: string | |
| action: string | |
| parameters: Record<string, unknown> | |
| } | |
| toolResult?: { | |
| success: boolean | |
| data?: unknown | |
| error?: string | |
| } | |
| isProcessing?: boolean | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState, useRef, useCallback } from "react"; | |
| interface SpeechRecognitionEvent { | |
| results: SpeechRecognitionResultList; | |
| resultIndex: number; | |
| } | |
| interface SpeechRecognition extends EventTarget { | |
| continuous: boolean; | |
| interimResults: boolean; | |
| lang: string; | |
| onstart: ((event: Event) => void) | null; | |
| onend: ((event: Event) => void) | null; | |
| onresult: ((event: SpeechRecognitionEvent) => void) | null; | |
| onerror: ((event: any) => void) | null; | |
| start(): void; | |
| stop(): void; | |
| abort(): void; | |
| } | |
| declare global { | |
| interface Window { | |
| SpeechRecognition: new () => SpeechRecognition; | |
| webkitSpeechRecognition: new () => SpeechRecognition; | |
| } | |
| } | |
| export const useVoiceRecognition = () => { | |
| const [isListening, setIsListening] = useState(false); | |
| const [transcript, setTranscript] = useState(""); | |
| const [isSupported, setIsSupported] = useState(false); | |
| const recognitionRef = useRef<SpeechRecognition | null>(null); | |
| const initializeRecognition = useCallback(() => { | |
| if (typeof window === "undefined") return false; | |
| const SpeechRecognition = | |
| window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SpeechRecognition) { | |
| setIsSupported(false); | |
| return false; | |
| } | |
| setIsSupported(true); | |
| recognitionRef.current = new SpeechRecognition(); | |
| const recognition = recognitionRef.current; | |
| recognition.continuous = false; | |
| recognition.interimResults = true; | |
| recognition.lang = "en-US"; | |
| recognition.onstart = () => { | |
| setIsListening(true); | |
| }; | |
| recognition.onend = () => { | |
| setIsListening(false); | |
| }; | |
| recognition.onresult = (event: SpeechRecognitionEvent) => { | |
| let finalTranscript = ""; | |
| let interimTranscript = ""; | |
| for (let i = event.resultIndex; i < event.results.length; i++) { | |
| const transcript = event.results[i][0].transcript; | |
| if (event.results[i].isFinal) { | |
| finalTranscript += transcript + " "; | |
| } else { | |
| interimTranscript += transcript; | |
| } | |
| } | |
| setTranscript(finalTranscript || interimTranscript); | |
| }; | |
| recognition.onerror = (event: any) => { | |
| console.error("Speech recognition error:", event.error); | |
| setIsListening(false); | |
| }; | |
| return true; | |
| }, []); | |
| const startListening = useCallback(() => { | |
| if (!recognitionRef.current && !initializeRecognition()) { | |
| return; | |
| } | |
| if (recognitionRef.current && !isListening) { | |
| setTranscript(""); | |
| recognitionRef.current.start(); | |
| } | |
| }, [isListening, initializeRecognition]); | |
| const stopListening = useCallback(() => { | |
| if (recognitionRef.current && isListening) { | |
| recognitionRef.current.stop(); | |
| } | |
| }, [isListening]); | |
| const resetTranscript = useCallback(() => { | |
| setTranscript(""); | |
| }, []); | |
| return { | |
| isListening, | |
| transcript, | |
| isSupported, | |
| startListening, | |
| stopListening, | |
| resetTranscript, | |
| }; | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use client' | |
| import { useState } from 'react' | |
| import { Button } from '@/components/ui/button' | |
| import { Input } from '@/components/ui/input' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' | |
| import { MessageCircle } from 'lucide-react' | |
| interface UsernameFormProps { | |
| onUsernameSubmit: (username: string) => void | |
| } | |
| export function UsernameForm({ onUsernameSubmit }: UsernameFormProps) { | |
| const [username, setUsername] = useState('') | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault() | |
| if (username.trim()) { | |
| onUsernameSubmit(username.trim()) | |
| } | |
| } | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800"> | |
| <Card className="w-full max-w-md"> | |
| <CardHeader className="text-center"> | |
| <div className="mx-auto mb-4 p-3 bg-primary/10 rounded-full w-fit"> | |
| <MessageCircle className="w-6 h-6 text-primary" /> | |
| </div> | |
| <CardTitle className="text-2xl font-bold">Join Chat</CardTitle> | |
| <CardDescription> | |
| Enter your username to start chatting with others | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Input | |
| placeholder="Enter your username" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| className="text-center" | |
| maxLength={20} | |
| required | |
| /> | |
| </div> | |
| <Button type="submit" className="w-full" disabled={!username.trim()}> | |
| Start Chatting | |
| </Button> | |
| </form> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment