diff --git a/submodules/moragents_dockers/agents/src/app.py b/submodules/moragents_dockers/agents/src/app.py index 6631953..f4074be 100644 --- a/submodules/moragents_dockers/agents/src/app.py +++ b/submodules/moragents_dockers/agents/src/app.py @@ -17,7 +17,7 @@ chat_manager_routes, key_manager_routes, wallet_manager_routes, - workflow_manager_routes + workflow_manager_routes, ) # Constants @@ -40,6 +40,8 @@ allow_methods=["*"], allow_headers=["*"], ) + + @app.on_event("startup") async def startup_event(): await workflow_manager_instance.initialize() @@ -119,18 +121,36 @@ def validate_agent_response(response: dict, current_agent: str) -> dict: @app.post("/chat") async def chat(chat_request: ChatRequest): - prompt = chat_request.prompt.dict() - chat_manager_instance.add_message(prompt) + """Send a chat message and get a response""" + logger.info(f"Received chat request for conversation {chat_request.conversation_id}") + + # Parse command if present + agent_name, message = agent_manager_instance.parse_command( + chat_request.prompt.dict()["content"] + ) + + if agent_name: + # If command was used, set that agent as active + agent_manager_instance.set_active_agent(agent_name) + # Update message content without command + chat_request.prompt.dict()["content"] = message + + chat_manager_instance.add_message(chat_request.prompt.dict(), chat_request.conversation_id) try: - delegator.reset_attempted_agents() - active_agent = await get_active_agent_for_chat(prompt) + if not agent_name: + delegator.reset_attempted_agents() + active_agent = await get_active_agent_for_chat(chat_request.prompt.dict()) + else: + active_agent = agent_name logger.info(f"Delegating chat to active agent: {active_agent}") current_agent, response = delegator.delegate_chat(active_agent, chat_request) validated_response = validate_agent_response(response, current_agent) - chat_manager_instance.add_response(validated_response, current_agent) + chat_manager_instance.add_response( + validated_response, current_agent, chat_request.conversation_id + ) logger.info(f"Sending response: {validated_response}") return validated_response diff --git a/submodules/moragents_dockers/agents/src/config.py b/submodules/moragents_dockers/agents/src/config.py index 4809fec..478f4a6 100644 --- a/submodules/moragents_dockers/agents/src/config.py +++ b/submodules/moragents_dockers/agents/src/config.py @@ -22,6 +22,7 @@ class Config: "description": "Must be used for meta-queries that ask about active Morpheus agents, and also for general, simple questions", "name": "default", "human_readable_name": "Default General Purpose", + "command": "morpheus", "upload_required": False, }, { @@ -30,6 +31,7 @@ class Config: "description": "Must only be used for image generation tasks. Use when the query explicitly mentions generating or creating an image.", "name": "imagen", "human_readable_name": "Image Generator", + "command": "imagen", "upload_required": False, }, { @@ -38,6 +40,7 @@ class Config: "description": "Handles transactions on the Base crypto network. Use when the user makes any reference to Base, base, the base network, or Coinbase", "name": "base", "human_readable_name": "Base Transaction Manager", + "command": "base", "upload_required": False, }, { @@ -46,6 +49,7 @@ class Config: "description": "Crypto-specific. Provides real-time cryptocurrency data such as price, market cap, and fully diluted valuation (FDV).", "name": "crypto data", "human_readable_name": "Crypto Data Fetcher", + "command": "crypto", "upload_required": False, }, # DISABLED: Pending 1inch protocol fix @@ -63,6 +67,7 @@ class Config: "description": "Generates engaging tweets. Use ONLY when the query explicitly mentions Twitter, tweeting, or the X platform.", "name": "tweet sizzler", "human_readable_name": "Tweet / X-Post Generator", + "command": "tweet", "upload_required": False, }, { @@ -71,6 +76,7 @@ class Config: "description": "Sets up DCA strategies. Use when the user requests to set up a dollar-cost averaging strategy for crypto purchases or trades.", "name": "dca", "human_readable_name": "DCA Strategy Manager", + "command": "dca", "upload_required": False, }, { @@ -79,6 +85,7 @@ class Config: "description": "Answers questions about a document. Must be used anytime an upload, a document, Documents, or uploaded document is mentioned", "name": "rag", "human_readable_name": "Document Assistant", + "command": "document", "upload_required": True, }, # DISABLED: @@ -96,6 +103,7 @@ class Config: "description": "Provides information about user's accrued MOR rewards or tokens. Use when the query is about checking or querying reward balances.", "name": "mor rewards", "human_readable_name": "MOR Rewards Tracker", + "command": "rewards", "upload_required": False, }, { @@ -104,6 +112,7 @@ class Config: "description": f"Use when the query is about searching the web or asks about a recent / current event (The year is {datetime.datetime.now().year})", "name": "realtime search", "human_readable_name": "Real-Time Search", + "command": "search", "upload_required": False, }, { @@ -112,6 +121,7 @@ class Config: "description": "Fetches and analyzes cryptocurrency news for potential price impacts.", "name": "crypto news", "human_readable_name": "Crypto News Analyst", + "command": "news", "upload_required": False, }, ] diff --git a/submodules/moragents_dockers/agents/src/models/messages.py b/submodules/moragents_dockers/agents/src/models/messages.py index 52d6880..df103ad 100644 --- a/submodules/moragents_dockers/agents/src/models/messages.py +++ b/submodules/moragents_dockers/agents/src/models/messages.py @@ -1,4 +1,5 @@ from typing import List, Optional +from fastapi import Query from pydantic import BaseModel @@ -11,6 +12,7 @@ class ChatRequest(BaseModel): prompt: ChatMessage chain_id: str wallet_address: str + conversation_id: str = Query(default="default") class ChatMessage(BaseModel): diff --git a/submodules/moragents_dockers/agents/src/routes/agent_manager_routes.py b/submodules/moragents_dockers/agents/src/routes/agent_manager_routes.py index 1b3cd4a..6b40af4 100644 --- a/submodules/moragents_dockers/agents/src/routes/agent_manager_routes.py +++ b/submodules/moragents_dockers/agents/src/routes/agent_manager_routes.py @@ -29,3 +29,18 @@ async def set_selected_agents(request: Request) -> JSONResponse: logger.info(f"Newly selected agents: {agent_manager_instance.get_selected_agents()}") return JSONResponse(content={"status": "success", "agents": agent_names}) + + +@router.get("/commands") +async def get_agent_commands() -> JSONResponse: + """Get the list of available agent commands""" + available_agents = agent_manager_instance.get_available_agents() + commands = [ + { + "command": agent["command"], + "description": agent["description"], + "name": agent["human_readable_name"], + } + for agent in available_agents + ] + return JSONResponse(content={"commands": commands}) diff --git a/submodules/moragents_dockers/agents/src/routes/chat_manager_routes.py b/submodules/moragents_dockers/agents/src/routes/chat_manager_routes.py index 1346647..32f41d2 100644 --- a/submodules/moragents_dockers/agents/src/routes/chat_manager_routes.py +++ b/submodules/moragents_dockers/agents/src/routes/chat_manager_routes.py @@ -22,14 +22,20 @@ async def clear_messages(conversation_id: str = Query(default="default")): return {"response": "successfully cleared message history"} +@router.get("/conversations") +async def get_conversations(): + """Get all conversation IDs""" + logger.info("Getting all conversation IDs") + return {"conversation_ids": chat_manager_instance.get_all_conversation_ids()} + + @router.post("/conversations") async def create_conversation(): """Create a new conversation""" - # The conversation will be created automatically when first accessed new_id = f"conversation_{len(chat_manager_instance.get_all_conversation_ids())}" - chat_manager_instance.get_messages(new_id) # This creates the conversation + conversation = chat_manager_instance.create_conversation(new_id) logger.info(f"Created new conversation with ID: {new_id}") - return {"conversation_id": new_id} + return {"conversation_id": new_id, "conversation": conversation} @router.delete("/conversations/{conversation_id}") diff --git a/submodules/moragents_dockers/agents/src/stores/agent_manager.py b/submodules/moragents_dockers/agents/src/stores/agent_manager.py index aaee3dd..a28cf61 100644 --- a/submodules/moragents_dockers/agents/src/stores/agent_manager.py +++ b/submodules/moragents_dockers/agents/src/stores/agent_manager.py @@ -1,7 +1,7 @@ import importlib import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from langchain_ollama import ChatOllama from langchain_community.embeddings import OllamaEmbeddings @@ -165,6 +165,41 @@ def get_agent(self, agent_name: str) -> Optional[Any]: """ return self.agents.get(agent_name) + def get_agent_by_command(self, command: str) -> Optional[str]: + """ + Get agent name by command. + + Args: + command (str): Command to look up + + Returns: + Optional[str]: Agent name if found, None otherwise + """ + for agent in self.config["agents"]: + if agent["command"] == command: + return agent["name"] + return None + + def parse_command(self, message: str) -> Tuple[Optional[str], str]: + """ + Parse a message for commands. + + Args: + message (str): Message to parse + + Returns: + Tuple[Optional[str], str]: Tuple of (agent_name, message_without_command) + """ + if not message.startswith("/"): + return None, message + + parts = message[1:].split(maxsplit=1) + command = parts[0] + remaining_message = parts[1] if len(parts) > 1 else "" + + agent_name = self.get_agent_by_command(command) + return agent_name, remaining_message + # Create an instance to act as a singleton store agent_manager_instance = AgentManager(Config.AGENTS_CONFIG) diff --git a/submodules/moragents_dockers/frontend/components/ChatInput/index.tsx b/submodules/moragents_dockers/frontend/components/ChatInput/index.tsx index edbcf29..3b75829 100644 --- a/submodules/moragents_dockers/frontend/components/ChatInput/index.tsx +++ b/submodules/moragents_dockers/frontend/components/ChatInput/index.tsx @@ -1,17 +1,25 @@ -import React, { FC, useState } from "react"; +import React, { FC, useState, useEffect, useRef } from "react"; import { - Flex, Textarea, InputGroup, InputLeftAddon, InputRightAddon, IconButton, + Box, + VStack, + Text, } from "@chakra-ui/react"; import { AttachmentIcon } from "@chakra-ui/icons"; import { SendIcon } from "../CustomIcon/SendIcon"; import PrefilledOptions from "./PrefilledOptions"; import styles from "./index.module.css"; +type Command = { + command: string; + description: string; + name: string; +}; + type ChatInputProps = { onSubmit: (message: string, file: File | null) => Promise; disabled: boolean; @@ -25,6 +33,94 @@ export const ChatInput: FC = ({ }) => { const [message, setMessage] = useState(""); const [file, setFile] = useState(null); + const [commands, setCommands] = useState([]); + const [showCommands, setShowCommands] = useState(false); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); + const [dropdownPosition, setDropdownPosition] = useState({ + top: 0, + left: 0, + width: 0, + }); + const inputGroupRef = useRef(null); + const inputRef = useRef(null); + const commandsRef = useRef(null); + + useEffect(() => { + fetch("http://localhost:8080/agents/commands") + .then((res) => res.json()) + .then((data) => setCommands(data.commands)) + .catch((error) => console.error("Error fetching commands:", error)); + }, []); + + useEffect(() => { + if (inputGroupRef.current && message.startsWith("/")) { + const rect = inputGroupRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.top, + left: rect.left - 400, + width: rect.width, + }); + } + }, [message, showCommands]); + + const filteredCommands = message.startsWith("/") + ? commands.filter((cmd) => + cmd.command.toLowerCase().includes(message.slice(1).toLowerCase()) + ) + : []; + + useEffect(() => { + setShowCommands(message.startsWith("/") && filteredCommands.length > 0); + setSelectedCommandIndex(0); + }, [message, filteredCommands.length]); + + useEffect(() => { + if (commandsRef.current && showCommands) { + const selectedElement = commandsRef.current.querySelector( + `[data-index="${selectedCommandIndex}"]` + ); + if (selectedElement) { + selectedElement.scrollIntoView({ + block: "center", + behavior: "smooth", + }); + } + } + }, [selectedCommandIndex, showCommands]); + + const handleCommandSelect = (command: Command) => { + setMessage(`/${command.command} `); + setShowCommands(false); + inputRef.current?.focus(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (showCommands) { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setSelectedCommandIndex((prev) => + Math.min(prev + 1, filteredCommands.length - 1) + ); + break; + case "ArrowUp": + e.preventDefault(); + setSelectedCommandIndex((prev) => Math.max(prev - 1, 0)); + break; + case "Tab": + case "Enter": + e.preventDefault(); + handleCommandSelect(filteredCommands[selectedCommandIndex]); + break; + case "Escape": + setShowCommands(false); + break; + } + } else if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; const agentSupportsFileUploads = true; @@ -41,10 +137,66 @@ export const ChatInput: FC = ({ return ( <> - {!hasMessages && } + {showCommands && ( + + + {filteredCommands.map((cmd, index) => ( + handleCommandSelect(cmd)} + transition="background-color 0.2s" + borderBottom="1px solid #454945" + _last={{ borderBottom: "none" }} + > + + /{cmd.command} + + + {cmd.name} - {cmd.description} + + + ))} + + + )} +
- - + {!hasMessages && } +
+ {agentSupportsFileUploads && ( = ({ )}