diff --git a/agent_repl.py b/agent_repl.py index 734dceb5b..520b918ba 100644 --- a/agent_repl.py +++ b/agent_repl.py @@ -1,20 +1,15 @@ -import argparse +import warnings from typing import List, Tuple -from fastapi import Depends +from dotenv import load_dotenv from aios.hooks.modules.agent import useFactory from aios.hooks.modules.llm import useCore from aios.hooks.modules.scheduler import useFIFOScheduler -from aios.utils.utils import delete_directories, humanify_agent, parse_global_args from aios.utils.state import useGlobalState - -from pyopenagi.agents.interact import Interactor +from aios.utils.utils import delete_directories, parse_global_args from pyopenagi.manager.manager import AgentManager -from dotenv import load_dotenv -import warnings - # load the args warnings.filterwarnings("ignore") parser = parse_global_args() diff --git a/agenthub/app/agents/ContentLayout/DatasetsHeader.tsx b/agenthub/app/agents/ContentLayout/DatasetsHeader.tsx index 905b87048..43b2d7326 100644 --- a/agenthub/app/agents/ContentLayout/DatasetsHeader.tsx +++ b/agenthub/app/agents/ContentLayout/DatasetsHeader.tsx @@ -40,4 +40,4 @@ export function DatasetsHeader({ filteredCount = 0 }: DatasetsHeaderProps) { ) -} \ No newline at end of file +} diff --git a/agenthub/app/agents/const.ts b/agenthub/app/agents/const.ts index ac0cdb966..50d074343 100644 --- a/agenthub/app/agents/const.ts +++ b/agenthub/app/agents/const.ts @@ -12,7 +12,7 @@ export const DatasetList: DatasetItem[] = [...Array(30)].map(() => ({ })) export const AgentList: AgentItem[] = []; -// export const AgentList = +// export const AgentList = export const AgentListGenerator: () => Promise = async () => { try { @@ -395,4 +395,4 @@ export const DatasetLanguages = [ 'xx', 'us', 'ua', -] \ No newline at end of file +] diff --git a/agenthub/app/agents/page.tsx b/agenthub/app/agents/page.tsx index 1966c3ce3..6be3a9022 100644 --- a/agenthub/app/agents/page.tsx +++ b/agenthub/app/agents/page.tsx @@ -8,4 +8,4 @@ export default function Datasets() { ) -} \ No newline at end of file +} diff --git a/agenthub/app/agents/type.ts b/agenthub/app/agents/type.ts index a3fb36828..67c4fe9b6 100644 --- a/agenthub/app/agents/type.ts +++ b/agenthub/app/agents/type.ts @@ -22,4 +22,4 @@ export interface AgentItem { createdAt: string; numDownloads: number; numFavorites: number; -} \ No newline at end of file +} diff --git a/agenthub/app/chat/page.tsx b/agenthub/app/chat/page.tsx index 4937d3ee6..8276e0641 100644 --- a/agenthub/app/chat/page.tsx +++ b/agenthub/app/chat/page.tsx @@ -83,7 +83,7 @@ const ChatInterface: React.FC = () => { thinking: false }; - setChats(prevChats => prevChats.map(chat => + setChats(prevChats => prevChats.map(chat => chat.id === activeChat ? { ...chat, messages: [...chat.messages, newMessage] } : chat @@ -98,7 +98,7 @@ const ChatInterface: React.FC = () => { thinking: true }; - setChats(prevChats => prevChats.map(chat => + setChats(prevChats => prevChats.map(chat => chat.id === activeChat ? { ...chat, messages: [...chat.messages, botMessage] } : chat @@ -106,11 +106,11 @@ const ChatInterface: React.FC = () => { const res = await processAgentCommand(parseNamedContent(parseText(content))[0] as AgentCommand); - setChats(prevChats => prevChats.map(chat => + setChats(prevChats => prevChats.map(chat => chat.id === activeChat ? { ...chat, - messages: chat.messages.map(message => + messages: chat.messages.map(message => message.id === messageId ? { ...message, thinking: false, text: res.content } : message @@ -186,8 +186,8 @@ const ChatInterface: React.FC = () => { // Add updateChatName function const updateChatName = (chatId: number, newName: string) => { - setChats(prevChats => - prevChats.map(chat => + setChats(prevChats => + prevChats.map(chat => chat.id === chatId ? { ...chat, name: newName } : chat ) ); @@ -217,10 +217,10 @@ const ChatInterface: React.FC = () => { darkMode={darkMode} />
-
chat.id === activeChat)?.name || 'Chat'} + title={chats.find(chat => chat.id === activeChat)?.name || 'Chat'} />
diff --git a/agenthub/components/agentchat/Header.tsx b/agenthub/components/agentchat/Header.tsx index 92cdc4ec5..aeb8a2468 100644 --- a/agenthub/components/agentchat/Header.tsx +++ b/agenthub/components/agentchat/Header.tsx @@ -29,4 +29,4 @@ export const Header: React.FC = ({ darkMode, setDarkMode, title })
); - }; \ No newline at end of file + }; diff --git a/agenthub/components/agentchat/Sidebar.tsx b/agenthub/components/agentchat/Sidebar.tsx index 6076569fb..56998246e 100644 --- a/agenthub/components/agentchat/Sidebar.tsx +++ b/agenthub/components/agentchat/Sidebar.tsx @@ -18,49 +18,49 @@ export interface SidebarProps { export const Sidebar: React.FC = ({ chats, activeChat, setActiveChat, addChat, updateChatName, deleteChat, darkMode }) => { const [editingId, setEditingId] = useState(null); const [editingName, setEditingName] = useState(''); - + const categoryStyle = "text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2 mt-4 px-2 flex justify-between items-center"; const channelStyle = `flex items-center justify-between rounded px-2 py-1.5 text-sm font-medium transition-colors duration-200 ease-in-out cursor-pointer`; const activeChannelStyle = darkMode ? 'bg-gray-700 text-white' : 'bg-gray-300 text-gray-900'; const inactiveChannelStyle = darkMode ? 'text-gray-400 hover:bg-gray-700 hover:text-gray-200' : 'text-gray-700 hover:bg-gray-200 hover:text-gray-900'; - + const startEditing = (chat: Chat) => { setEditingId(chat.id); setEditingName(chat.name); }; - + const cancelEditing = () => { setEditingId(null); setEditingName(''); }; - + const saveEditing = () => { if (editingId !== null && editingName.trim() !== '') { updateChatName(editingId, editingName.trim()); setEditingId(null); } }; - + const handleDelete = (chatId: number, e: React.MouseEvent) => { e.stopPropagation(); if (window.confirm('Are you sure you want to delete this channel?')) { deleteChat(chatId); } }; - + return (

Your AIOS Workspace

- +
Channels - @@ -99,32 +99,32 @@ export const Sidebar: React.FC = ({ chats, activeChat, setActiveCh
- { e.stopPropagation(); startEditing(chat); - }} - variant="subtle" + }} + variant="subtle" color={darkMode ? "gray" : "dark"} size="sm" className="ml-1" > - - handleDelete(chat.id, e)} - variant="subtle" + handleDelete(chat.id, e)} + variant="subtle" size="sm" className="ml-1" > - diff --git a/agenthub/components/homepage/Products.tsx b/agenthub/components/homepage/Products.tsx index 14f93730e..9325d6921 100644 --- a/agenthub/components/homepage/Products.tsx +++ b/agenthub/components/homepage/Products.tsx @@ -49,7 +49,7 @@ const BuiltWithSupabase = () => {
Try out our many features
- +
{Examples.slice(0, 2).map((example: any, i: number) => { diff --git a/agenthub/interfaces/agentchat.ts b/agenthub/interfaces/agentchat.ts index 9ea292430..b68edeaf1 100644 --- a/agenthub/interfaces/agentchat.ts +++ b/agenthub/interfaces/agentchat.ts @@ -11,4 +11,4 @@ export interface Chat { id: number; name: string; messages: Message[]; -} \ No newline at end of file +} diff --git a/agenthub/lib/env.ts b/agenthub/lib/env.ts index 1376dba94..d05f10d20 100644 --- a/agenthub/lib/env.ts +++ b/agenthub/lib/env.ts @@ -1,6 +1,9 @@ export const inDevEnvironment = !!process && process.env.NODE_ENV === 'development'; // export const serverUrl = inDevEnvironment ? 'http://localhost:8000' : 'https://myapp-y5z35kuonq-uk.a.run.app' -export const baseUrl = inDevEnvironment ? 'http://localhost:3000' : 'https://my.aios.foundation' +export const baseUrl = process.env.NODE_ENV === 'development' + ? 'http://localhost:3000' + : 'https://my.aios.foundation'; // export const serverUrl = inDevEnvironment ? 'http://localhost:8000' : 'http://35.232.56.61:8000' -export const serverUrl = 'http://35.232.56.61:8000'; - +export const serverUrl = process.env.NODE_ENV === 'development' + ? 'http://localhost:8000' + : 'https://api.aios.chat'; diff --git a/agenthub/public/MentionListV2Light.scss b/agenthub/public/MentionListV2Light.scss index 710352c00..cd52d6728 100644 --- a/agenthub/public/MentionListV2Light.scss +++ b/agenthub/public/MentionListV2Light.scss @@ -10,7 +10,7 @@ overflow: auto; padding: 0.4rem; position: relative; - + button { align-items: center; background-color: transparent; @@ -18,7 +18,7 @@ gap: 0.25rem; text-align: left; width: 100%; - + .item-text { color: #000000 !important; } @@ -27,7 +27,7 @@ &:hover.is-selected { background-color: var(--gray-3); } - + &.is-selected { background-color: var(--gray-2); } @@ -38,7 +38,7 @@ :first-child { margin-top: 0; } - + .mention { background-color: var(--purple-light); border-radius: 0.4rem; @@ -54,4 +54,4 @@ box-decoration-break: clone; color: var(--purple); padding: 0.1rem 0.3rem; - } \ No newline at end of file + } diff --git a/aios/context/simple_context.py b/aios/context/simple_context.py index aa5da7e15..8822c7d97 100644 --- a/aios/context/simple_context.py +++ b/aios/context/simple_context.py @@ -3,12 +3,8 @@ from aios.context.base import BaseContextManager -import os - -import torch # import shutil -from threading import Lock class SimpleContextManager(BaseContextManager): def __init__(self): @@ -21,7 +17,7 @@ def start(self): def gen_snapshot(self, pid, context): # file_path = os.path.join(self.context_dir, f"process-{pid}.pt") # torch.save(context, file_path) - self.context_dict[str(pid)] = context + self.context_dic[str(pid)] = context def gen_recover(self, pid): # file_path = os.path.join(self.context_dir, f"process-{pid}.pt") diff --git a/aios/hooks/llm.py b/aios/hooks/llm.py index 90a0c2e01..41afc959b 100644 --- a/aios/hooks/llm.py +++ b/aios/hooks/llm.py @@ -139,7 +139,9 @@ def aios_starter( max_gpu_memory, eval_device, max_new_tokens, - log_mode, + scheduler_log_mode, + agent_log_mode, + llm_kernel_log_mode, use_backend ): """ @@ -151,7 +153,9 @@ def aios_starter( max_gpu_memory (str): The maximum amount of GPU memory to use. eval_device (str): The device to evaluate the LLM on. max_new_tokens (int): The maximum number of new tokens to generate. - log_mode (str): The log mode. + agent_log_mode: + llm_kernel_log_mode: + scheduler_log_mode: use_backend (str): The backend to use for running the LLM kernel. Yields: @@ -164,15 +168,15 @@ def aios_starter( max_gpu_memory=max_gpu_memory, eval_device=eval_device, max_new_tokens=max_new_tokens, - log_mode=log_mode, + log_mode=llm_kernel_log_mode, use_backend=use_backend ) # run agents concurrently for maximum efficiency using a scheduler submit_agent, await_agent_execution = useFactory( - log_mode=log_mode, + log_mode=agent_log_mode, max_workers=64 ) - with fifo_scheduler(llm=llm, log_mode=log_mode, get_queue_message=None): + with fifo_scheduler(llm=llm, log_mode=scheduler_log_mode, get_queue_message=None): yield submit_agent, await_agent_execution diff --git a/aios/hooks/modules/scheduler.py b/aios/hooks/modules/scheduler.py index 89d46556d..1366d15c2 100644 --- a/aios/hooks/modules/scheduler.py +++ b/aios/hooks/modules/scheduler.py @@ -1,7 +1,6 @@ -from typing import Any, Tuple, Callable, Dict -from random import randint +from contextlib import contextmanager +from typing import Tuple, Callable -from aios.llm_core.llms import LLM from aios.hooks.types.scheduler import ( # AgentSubmitDeclaration, # FactoryParams, @@ -12,11 +11,7 @@ # QueueAddMessage, # QueueCheckEmpty, ) - -from contextlib import contextmanager - from aios.hooks.utils.validate import validate -from aios.hooks.stores import queue as QueueStore, processes as ProcessStore from aios.scheduler.fifo_scheduler import FIFOScheduler @@ -36,15 +31,15 @@ def useFIFOScheduler( if params.get_llm_syscall is None: from aios.hooks.stores._global import global_llm_req_queue_get_message params.get_llm_syscall = global_llm_req_queue_get_message - + if params.get_memory_syscall is None: from aios.hooks.stores._global import global_memory_req_queue_get_message params.get_memory_syscall = global_memory_req_queue_get_message - + if params.get_storage_syscall is None: from aios.hooks.stores._global import global_storage_req_queue_get_message params.get_storage_syscall = global_storage_req_queue_get_message - + if params.get_tool_syscall is None: from aios.hooks.stores._global import global_tool_req_queue_get_message params.get_tool_syscall = global_tool_req_queue_get_message @@ -78,15 +73,15 @@ def fifo_scheduler(params: SchedulerParams): if params.get_memory_syscall is None: from aios.hooks.stores._global import global_memory_req_queue_get_message params.get_memory_syscall = global_memory_req_queue_get_message - + if params.get_storage_syscall is None: from aios.hooks.stores._global import global_storage_req_queue_get_message params.get_storage_syscall = global_storage_req_queue_get_message - + if params.get_tool_syscall is None: from aios.hooks.stores._global import global_tool_req_queue_get_message params.get_tool_syscall = global_tool_req_queue_get_message - + scheduler = FIFOScheduler(**params.model_dump()) scheduler.start() diff --git a/aios/hooks/starter.py b/aios/hooks/starter.py index f5962919e..846406a8b 100644 --- a/aios/hooks/starter.py +++ b/aios/hooks/starter.py @@ -47,7 +47,7 @@ def aios_starter( root_dir = "root", use_vector_db = False ) - + memory_manager = useMemoryManager( memory_limit = 100*1024*1024, eviction_k = 10, diff --git a/aios/hooks/syscall.py b/aios/hooks/syscall.py index 0a46e466b..0955376b1 100755 --- a/aios/hooks/syscall.py +++ b/aios/hooks/syscall.py @@ -1,17 +1,15 @@ -from threading import Thread, Lock, Event -from typing import Mapping - -import random import time +from threading import Thread, Event + from aios.hooks.stores._global import ( global_llm_req_queue_add_message, global_memory_req_queue_add_message, global_storage_req_queue_add_message, global_tool_req_queue_add_message, ) - from pyopenagi.utils.chat_template import Request, LLMQuery, MemoryQuery, StorageQuery, ToolQuery + class Message(Request): pass diff --git a/aios/hooks/types/scheduler.py b/aios/hooks/types/scheduler.py index 71a1c5c11..cff02dda4 100644 --- a/aios/hooks/types/scheduler.py +++ b/aios/hooks/types/scheduler.py @@ -1,11 +1,13 @@ +from typing import Any + from pydantic import BaseModel -from typing import Any, TypeAlias, Callable from .llm import LLMRequestQueueGetMessage from .memory import MemoryRequestQueueGetMessage from .storage import StorageRequestQueueGetMessage from .tool import ToolRequestQueueGetMessage + class SchedulerParams(BaseModel): llm: Any memory_manager: Any @@ -15,4 +17,4 @@ class SchedulerParams(BaseModel): get_llm_syscall: LLMRequestQueueGetMessage | None get_memory_syscall: MemoryRequestQueueGetMessage | None get_storage_syscall: StorageRequestQueueGetMessage | None - get_tool_syscall: ToolRequestQueueGetMessage | None \ No newline at end of file + get_tool_syscall: ToolRequestQueueGetMessage | None diff --git a/aios/llm_core/llm_classes/gemini_llm.py b/aios/llm_core/llm_classes/gemini_llm.py index 488c5b93d..c7d47645f 100644 --- a/aios/llm_core/llm_classes/gemini_llm.py +++ b/aios/llm_core/llm_classes/gemini_llm.py @@ -90,7 +90,7 @@ def address_syscall(self, llm_syscall, temperature=0.0) -> None: tool_calls = self.parse_tool_calls(result) if tool_calls: response = Response( - response_message=None, + response_message=None, tool_calls=tool_calls, finished=True ) diff --git a/aios/llm_core/llm_classes/gpt_llm.py b/aios/llm_core/llm_classes/gpt_llm.py index a3c0e2266..ec1296fe4 100644 --- a/aios/llm_core/llm_classes/gpt_llm.py +++ b/aios/llm_core/llm_classes/gpt_llm.py @@ -65,7 +65,7 @@ def address_syscall(self, llm_syscall, temperature=0.0): response = self.llm_generate(llm_syscall) else: query = llm_syscall.query - print(query) + # print(query) response = self.model.chat.completions.create( model=self.model_name, messages=query.messages, @@ -75,8 +75,8 @@ def address_syscall(self, llm_syscall, temperature=0.0): ) response_message = response.choices[0].message.content - - print(response_message) + + # print(response_message) tool_calls = self.parse_tool_calls( response.choices[0].message.tool_calls ) diff --git a/aios/llm_cores/adapter.py b/aios/llm_cores/adapter.py index 3c49ae796..83fb505c2 100644 --- a/aios/llm_cores/adapter.py +++ b/aios/llm_cores/adapter.py @@ -78,6 +78,3 @@ def __init__(self, def get_model(self) -> BaseLLM | None: return self.model - - - \ No newline at end of file diff --git a/aios/llm_cores/base.py b/aios/llm_cores/base.py index f5a0b346b..2b0024588 100644 --- a/aios/llm_cores/base.py +++ b/aios/llm_cores/base.py @@ -23,7 +23,7 @@ def __init__(self, self.load_llm_and_tokenizer() - + def convert_map(self, map: dict) -> dict: """ helper utility to convert the keys of a map to int """ @@ -116,4 +116,4 @@ def execute(self, query: Query): @abstractmethod def process(self, query: Query): - raise NotImplementedError \ No newline at end of file + raise NotImplementedError diff --git a/aios/llm_cores/providers/api/google.py b/aios/llm_cores/providers/api/google.py index 930bd049a..5c152ab1e 100644 --- a/aios/llm_cores/providers/api/google.py +++ b/aios/llm_cores/providers/api/google.py @@ -1,8 +1,7 @@ # wrapper around gemini from google for LLMs -import re -import time import json +import re from cerebrum.llm.base import BaseLLM from cerebrum.utils.chat import Query, Response diff --git a/aios/llm_cores/providers/api/openai.py b/aios/llm_cores/providers/api/openai.py index a6e002f1a..1ca3321b7 100644 --- a/aios/llm_cores/providers/api/openai.py +++ b/aios/llm_cores/providers/api/openai.py @@ -1,14 +1,12 @@ +import json import re -import time - -# could be dynamically imported similar to other models -from openai import OpenAI from cerebrum.llm.base import BaseLLM from cerebrum.utils.chat import Query, Response import openai -import json +# could be dynamically imported similar to other models +from openai import OpenAI class GPTLLM(BaseLLM): diff --git a/aios/llm_cores/registry.py b/aios/llm_cores/registry.py index 7100a489b..4540db29d 100644 --- a/aios/llm_cores/registry.py +++ b/aios/llm_cores/registry.py @@ -22,7 +22,7 @@ # claude 'claude-3-5-sonnet-20240620': ClaudeLLM, - + # amazon bedrock # 'bedrock/anthropic.claude-3-haiku-20240307-v1:0': BedrockLLM, @@ -30,4 +30,4 @@ # 'llama3-groq-8b-8192-tool-use-preview': GroqLLM, # 'llama3-70b-8192': GroqLLM, # 'mixtral-8x7b-32768' : GroqLLM -} \ No newline at end of file +} diff --git a/aios/scheduler/base.py b/aios/scheduler/base.py index 7d195c72c..b6ebe47f8 100644 --- a/aios/scheduler/base.py +++ b/aios/scheduler/base.py @@ -1,13 +1,12 @@ +from abc import abstractmethod +from threading import Thread + from aios.hooks.types.llm import LLMRequestQueueGetMessage from aios.hooks.types.memory import MemoryRequestQueueGetMessage -from aios.hooks.types.tool import ToolRequestQueueGetMessage from aios.hooks.types.storage import StorageRequestQueueGetMessage - +from aios.hooks.types.tool import ToolRequestQueueGetMessage from aios.utils.logger import SchedulerLogger -from abc import ABC, abstractmethod - -from threading import Thread class Scheduler: def __init__( @@ -60,11 +59,11 @@ def setup_logger(self): @abstractmethod def run_llm_syscall(self): pass - + @abstractmethod def run_memory_syscall(self): pass - + @abstractmethod def run_storage_syscall(self): pass diff --git a/aios/scheduler/fifo_scheduler.py b/aios/scheduler/fifo_scheduler.py index d34b200b5..3e0353af1 100644 --- a/aios/scheduler/fifo_scheduler.py +++ b/aios/scheduler/fifo_scheduler.py @@ -71,7 +71,7 @@ def run_memory_syscall(self): memory_syscall.set_status("executing") self.logger.log( - f"{mem_syscall.agent_name} is executing. \n", "execute" + f"{memory_syscall.agent_name} is executing. \n", "execute" ) memory_syscall.set_start_time(time.time()) diff --git a/aios/scheduler/rr_scheduler.py b/aios/scheduler/rr_scheduler.py index d8c6f1360..ca8a20083 100644 --- a/aios/scheduler/rr_scheduler.py +++ b/aios/scheduler/rr_scheduler.py @@ -2,29 +2,20 @@ # Allows multiple agents to run at the same time, with each getting a fixed # chunk of processor time -from .base import BaseScheduler - # allows for memory to be shared safely between threads -from queue import Queue, Empty -from ..context.simple_context import SimpleContextManager +import time +import traceback +from queue import Empty from aios.hooks.types.llm import LLMRequestQueueGetMessage from aios.hooks.types.memory import MemoryRequestQueueGetMessage -from aios.hooks.types.tool import ToolRequestQueueGetMessage from aios.hooks.types.storage import StorageRequestQueueGetMessage - -from queue import Queue, Empty - -import traceback -import time -from aios.utils.logger import SchedulerLogger - -from threading import Thread - +from aios.hooks.types.tool import ToolRequestQueueGetMessage from .base import Scheduler +from ..context.simple_context import SimpleContextManager class RRScheduler(Scheduler): diff --git a/aios/sdk/interpreter/adapter.py b/aios/sdk/interpreter/adapter.py index ff8e2fe0a..e790e9d96 100644 --- a/aios/sdk/interpreter/adapter.py +++ b/aios/sdk/interpreter/adapter.py @@ -66,13 +66,13 @@ def adapter_aios_completions(**params): for attempt in range(attempts): try: - response, _, _, _, _ = send_request( + response = send_request( agent_name="Open-Interpreter", query=LLMQuery( messages=params['messages'], tools=(params["tools"] if "tools" in params else None) ) - ) + )["response"] # format similar to completion in interpreter comletion = {'choices': diff --git a/aios/storage/db_storage.py b/aios/storage/db_storage.py new file mode 100644 index 000000000..7ee05bc29 --- /dev/null +++ b/aios/storage/db_storage.py @@ -0,0 +1,59 @@ +# TODO: Not implemented +# Storing to databases has not been implemented yet + +import zlib +import pickle +import os + +class StorageManager: + def __init__(self, storage_path="persistent_storage", vector_db=None): + self.storage_path = storage_path + os.makedirs(self.storage_path, exist_ok=True) + self.vector_db = vector_db # Reference to the ChromaDB instance + + def sto_create(self, aname): + """Creates a new storage file and initializes a collection in vector_db for the agent.""" + file_path = os.path.join(self.storage_path, f"{aname}.dat") + if not os.path.exists(file_path): + with open(file_path, "wb") as file: + file.write(b"") + # Create a collection in the vector database for this agent + if self.vector_db: + self.vector_db.create_collection(aname) + + def sto_read(self, aname): + """Reads and decompresses data for a specific agent from the physical storage file.""" + file_path = os.path.join(self.storage_path, f"{aname}.dat") + if os.path.exists(file_path): + with open(file_path, "rb") as file: + compressed_data = file.read() + return pickle.loads(zlib.decompress(compressed_data)) if compressed_data else None + return None + + def sto_write(self, aname, s): + """Writes data both to a file and to the vector database.""" + # Write data to physical file storage + file_path = os.path.join(self.storage_path, f"{aname}.dat") + with open(file_path, "ab") as file: # Append mode + compressed_data = zlib.compress(pickle.dumps(s)) + file.write(compressed_data) + + # Append data to the vector database, if available + if self.vector_db: + self.vector_db.add(aname, s) # Assuming vector_db.add method supports appending data by agent name + + def sto_clear(self, aname): + """Clears the physical file storage and deletes the vector database collection for an agent.""" + file_path = os.path.join(self.storage_path, f"{aname}.dat") + if os.path.exists(file_path): + os.remove(file_path) + + # Clear data from vector database if available + if self.vector_db: + self.vector_db.delete(aname) + + def sto_retrieve(self, aname, query): + """Retrieves data from the vector database based on agent name and a query.""" + if self.vector_db: + return self.vector_db.retrieve(aname, query) + return None diff --git a/aios/storage/storage_classes/db_storage.py b/aios/storage/storage_classes/db_storage.py index 5c7166642..a534514d3 100644 --- a/aios/storage/storage_classes/db_storage.py +++ b/aios/storage/storage_classes/db_storage.py @@ -1,14 +1,8 @@ -from chromadb.config import Settings -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler - import os import chromadb +from llama_index.core import SimpleDirectoryReader -from llama_index.embeddings.huggingface import HuggingFaceEmbedding - -from llama_index.core import VectorStoreIndex, SimpleDirectoryReader class ChromaDB: def __init__(self, mount_dir) -> None: @@ -79,4 +73,4 @@ def delete_file_from_collection(self, client, collection_name, file_path): def retrieve(self, name, k, keywords): results = self.collection.query(query_texts=[keywords], n_results=int(k)) print([doc[:500] for doc in results["documents"][0]]) - print(results["metadatas"]) \ No newline at end of file + print(results["metadatas"]) diff --git a/aios/tool/manager.py b/aios/tool/manager.py index 7d93bc5c8..3f273cbec 100644 --- a/aios/tool/manager.py +++ b/aios/tool/manager.py @@ -26,7 +26,7 @@ def address_request(self, syscall) -> None: tool_result = tool.run(params=tool_params) self.tool_conflict_map.pop(tool_org_and_name) - + return Response( response_message=tool_result, finished=True diff --git a/experiment/agent/interpreter.py b/experiment/agent/interpreter.py index 61cfbea33..27e3d0e4d 100644 --- a/experiment/agent/interpreter.py +++ b/experiment/agent/interpreter.py @@ -63,7 +63,7 @@ def run(self, input_str: str): class InterpreterAgentHumanEval(ExperimentAgent): - SYSTEM_PROMPT = """You are an expert good at solving code problems. + SYSTEM_PROMPT = """You are an expert good at solving code problems. You will receive a function definition and comments. You need to help me complete this function. Give me final output in the format: diff --git a/pyopenagi/agents/experiment/standard/action/action_code.py b/experiment/benchmark/__init__.py similarity index 100% rename from pyopenagi/agents/experiment/standard/action/action_code.py rename to experiment/benchmark/__init__.py diff --git a/experiment/benchmark/gaia/__init__.py b/experiment/benchmark/gaia/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/experiment/benchmark/gaia/init_data.py b/experiment/benchmark/gaia/init_data.py new file mode 100644 index 000000000..2141f4654 --- /dev/null +++ b/experiment/benchmark/gaia/init_data.py @@ -0,0 +1,25 @@ +import os + +from huggingface_hub import snapshot_download + +DOWNLOAD_PATH = os.path.dirname(os.path.realpath(__file__)) +REPO_PATH = os.path.join(DOWNLOAD_PATH, "gaia") + + +def download_gaia(): + """Download the GAIA benchmark from Hugging Face.""" + + if not os.path.isdir(DOWNLOAD_PATH): + os.mkdir(DOWNLOAD_PATH) + + """Download the GAIA dataset from Hugging Face Hub""" + snapshot_download( + repo_id="gaia-benchmark/GAIA", + repo_type="dataset", + local_dir_use_symlinks=True, + local_dir=REPO_PATH, + ) + + +if __name__ == "__main__": + download_gaia() diff --git a/experiment/benchmark/gaia/run_eval.py b/experiment/benchmark/gaia/run_eval.py new file mode 100644 index 000000000..95f5e05b9 --- /dev/null +++ b/experiment/benchmark/gaia/run_eval.py @@ -0,0 +1,37 @@ +import json +from argparse import ArgumentParser +from experiment.benchmark.gaia.run_infer import prepare_dataset + + +def run_eval(input_file: str, output_file: str): + dataset = prepare_dataset() + dataset_map = {data["task_id"]: data for data in dataset} + with open(input_file, "r", encoding="utf-8") as file: + predictions = [json.loads(line) for line in file] + + pass_num = 0 + for prediction in predictions: + task_id = prediction["task_id"] + result = prediction["result"] + true_result = dataset_map[task_id]["Final answer"] + if result == true_result: + prediction["pass"] = True + pass_num += 1 + else: + prediction["pass"] = False + prediction["true_result"] = true_result + + with open(output_file, "w") as file: + for line in predictions: + json_line = json.dumps(line) + file.write(json_line + "\n") + + print(f"Gaia passed: {pass_num}, total: {len(predictions)}, pass rate: {pass_num/len(predictions)}") + + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument("--input_file", type=str, default="./experiment/benchmark/gaia/predictions.jsonl") + parser.add_argument("--output_file", type=str, default="./experiment/benchmark/gaia/report.jsonl") + args = parser.parse_args() + run_eval(args.input_file, args.output_file) diff --git a/experiment/benchmark/gaia/run_infer.py b/experiment/benchmark/gaia/run_infer.py new file mode 100644 index 000000000..b170cdc05 --- /dev/null +++ b/experiment/benchmark/gaia/run_infer.py @@ -0,0 +1,161 @@ +import json +import os +import re +from concurrent.futures import ThreadPoolExecutor, as_completed + +from tqdm import tqdm + +from aios.hooks.starter import aios_starter +from aios.utils.utils import parse_global_args +from experiment.benchmark.gaia.init_data import REPO_PATH +from pyopenagi.agents.experiment.standard.agent import StandardAgent + +DATA_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "gaia", + "2023", + "validation" +) + +SYSTEM_PROMPT = """You are a general AI assistant. I will ask you a question. Report your thoughts, and finish +your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. +YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of +numbers and/or strings. +If you are asked for a number, don’t use comma to write your number neither use units such as $ or percent +sign unless specified otherwise. +If you are asked for a string, don’t use articles, neither abbreviations (e.g. for cities), and write the digits in +plain text unless specified otherwise. +If you are asked for a comma separated list, apply the above rules depending of whether the element to be put +in the list is a number or a string. +""" + +FILE_PROMPT = """The current task is related to a file, and you may need to read the content of the file first. +The file path is {path}. +""" + +FILE_FOLDER = os.path.join(REPO_PATH, "2023", "validation") + + +class GaiaExpAgent(StandardAgent): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def custom_terminate(self) -> bool: + if self.rounds > 10: + return True + return True if "FINAL ANSWER" in self.short_term_memory.last_message()["content"] else False + + def custom_prompt(self) -> str: + return SYSTEM_PROMPT + + +def process_one_func(data): + question = data["Question"] + if data["file_name"]: + file_path = FILE_FOLDER + "/" + data["file_name"] + absolute_path = os.path.abspath(file_path) + question += ("\n" + FILE_PROMPT.format(path=absolute_path)) + + agent = GaiaExpAgent("Standard Agent", question) + result = agent.run() + + match = re.search(r"FINAL ANSWER:\s*(.*)", result["result"]) + # Extract the content if a match is found + if match: + final_answer = match.group(1) + else: + final_answer = result["result"] + + prediction = { + "task_id": data["task_id"], + "level": data["Level"], + "result": final_answer + } + print(f"Finished Task: \n{prediction}") + return prediction + + +def prepare_dataset(task_id: str = None): + input_file = os.path.join(DATA_PATH, "metadata.jsonl") + with open(input_file, "r") as file: + dataset = [json.loads(line) for line in file] + + if task_id is not None: + for data in dataset: + if data["task_id"] == task_id: + return data + return dataset + + +def run_infer(outputfile: str, workers: int, level: int, aios_args: dict): + dataset = prepare_dataset() + with aios_starter(**aios_args): + with ThreadPoolExecutor(max_workers=workers) as executor: + + futures = [] + for data in dataset: + # submit task + if level and data["Level"] != level: + continue + + futures.append( + executor.submit(process_one_func, data) + ) + + results = [] + + # Obtain infer result + for future in tqdm(as_completed(futures), total=len(futures), desc="Finished"): + results.append(future.result()) + + # Write result into .jsonl file + with open(outputfile, "w") as file: + for line in results: + json_line = json.dumps(line) + file.write(json_line + "\n") + + +def run_infer_specify_task(outputfile: str, task_id: str, aios_args: dict): + data = prepare_dataset(task_id=task_id) + with aios_starter(**aios_args): + result = process_one_func(data) + + # Write result into .jsonl file + with open(outputfile, "w", encoding="utf-8") as file: + json_line = json.dumps(result) + file.write(json_line + "\n") + + +if __name__ == '__main__': + parser = parse_global_args() + parser.add_argument("--output_file", type=str, default="./experiment/benchmark/gaia/predictions.jsonl") + parser.add_argument("--workers", type=int, default=1) + parser.add_argument("--task_id", type=str, default=None) + parser.add_argument("--level", type=int, default=None) + + args = parser.parse_args() + aios_args = { + "llm_name": args.llm_name, + "max_gpu_memory": args.max_gpu_memory, + "eval_device": args.eval_device, + "max_new_tokens": args.max_new_tokens, + "scheduler_log_mode": args.scheduler_log_mode, + "agent_log_mode": args.agent_log_mode, + "llm_kernel_log_mode": args.llm_kernel_log_mode, + "use_backend": args.use_backend, + } + + if args.task_id is not None: + run_infer_specify_task( + args.output_file, + args.task_id, + aios_args + ) + else: + run_infer( + args.output_file, + args.workers, + args.level, + aios_args + ) diff --git a/experiment/gaia/inference.py b/experiment/gaia/inference.py index ebb54f7e3..f9379406a 100644 --- a/experiment/gaia/inference.py +++ b/experiment/gaia/inference.py @@ -3,9 +3,10 @@ from typing import List from datasets import load_dataset + from aios.hooks.starter import aios_starter from experiment.agent.experiment_agent import ExperimentAgent -from experiment.experiment_core import MetaData, run_inference, AGENT_TYPE_MAPPING_AIOS, logger +from experiment.experiment_core import MetaData, AGENT_TYPE_MAPPING_AIOS, logger from experiment.utils import get_args @@ -50,7 +51,7 @@ def process_one_func(data, meta_data: MetaData): agent_type = "gaia:" + main_args.agent_type dataset = load_dataset(main_args.data_name, "2023_all", split=main_args.split) - + print(dataset[:1]) # meta = MetaData( diff --git a/main.py b/main.py index 86249c659..9ae38285a 100644 --- a/main.py +++ b/main.py @@ -90,4 +90,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/pyopenagi/agents/example/seeact_agent/agent.py b/pyopenagi/agents/example/seeact_agent/agent.py index db83b6670..fa8a54e03 100644 --- a/pyopenagi/agents/example/seeact_agent/agent.py +++ b/pyopenagi/agents/example/seeact_agent/agent.py @@ -1,9 +1,11 @@ import time -from pyopenagi.utils.logger import AgentLogger + from seeact.agent import SeeActAgent as SeeActCore -from seeact.demo_utils.inference_engine import Engine -from pyopenagi.utils.chat_template import LLMQuery + from aios.hooks.syscall import send_request +from pyopenagi.utils.chat_template import LLMQuery +from pyopenagi.utils.logger import AgentLogger + class SeeActAgent: def __init__(self, agent_name, task_input, log_mode: str): @@ -29,7 +31,7 @@ def __init__(self, agent_name, task_input, log_mode: str): ) # Replace the generate method - def custom_generate(self_engine, prompt: list = None, max_new_tokens=4096, temperature=None, + def custom_generate(self_engine, prompt: list = None, max_new_tokens=4096, temperature=None, model=None, image_path=None, ouput_0=None, turn_number=0, **kwargs): try: # Keep the original rate limiting logic @@ -76,14 +78,14 @@ def custom_generate(self_engine, prompt: list = None, max_new_tokens=4096, tempe ) return response.response_message - + except Exception as e: print(f"Error in generate: {str(e)}") raise # Replace the engine's generate method self.seeact.engine.generate = custom_generate.__get__(self.seeact.engine) - + self.start_time = None self.end_time = None self.created_time = time.time() @@ -92,7 +94,7 @@ async def run(self): try: self.start_time = time.time() await self.seeact.start() - + while not self.seeact.complete_flag: try: prediction_dict = await self.seeact.predict() @@ -100,16 +102,16 @@ async def run(self): await self.seeact.execute(prediction_dict) except Exception as e: self.logger.log(f"Error occurred: {e}", "info") - + await self.seeact.stop() self.end_time = time.time() - + return { "agent_name": self.agent_name, "result": "Task completed", "turnaround_time": self.end_time - self.start_time } - + except Exception as e: self.logger.log(f"Error in run method: {str(e)}", "info") return { @@ -118,4 +120,4 @@ async def run(self): } # Register the agent -SeeactAgent = SeeActAgent \ No newline at end of file +SeeactAgent = SeeActAgent diff --git a/pyopenagi/agents/experiment/standard/action/action.py b/pyopenagi/agents/experiment/standard/action/action.py index 56b62c265..bf63aab0c 100644 --- a/pyopenagi/agents/experiment/standard/action/action.py +++ b/pyopenagi/agents/experiment/standard/action/action.py @@ -10,3 +10,7 @@ def __call__(self, *args, **kwargs): @abstractmethod def format_prompt(self): pass + + @staticmethod + def display(): + pass diff --git a/pyopenagi/agents/experiment/standard/action/code.py b/pyopenagi/agents/experiment/standard/action/code.py new file mode 100644 index 000000000..601664fb9 --- /dev/null +++ b/pyopenagi/agents/experiment/standard/action/code.py @@ -0,0 +1,82 @@ +from pyopenagi.agents.experiment.standard.action.action import Action +from pyopenagi.agents.experiment.standard.environment.code_environment import CodeEnvironment + +CODE_PROMPT = """You can write code to solve problem. If you want to write code to solve problem, surrounding your +code with a code block. A perfect code should contains +- a function defination, like: + def calculate(a: int, b: int): + return a + b +- a call of function, like: + a = 5 + b = 10 + result = calculate(a, b) + print(f"Code execute result is: {result}") + +A code block should like: +```python +def calculate(a: int, b: int): + return a + b + +a = 5 +b = 10 +print(f"Code execute result is: {calculate(a, b)}") +``` + +If additional dependencies are required, please provid the command install them, format like: + +```requirement +pip install torch +pip install numpy +``` +""" + + +class ActionCode(Action): + """ + Action responsible for writing code to solve problem. + """ + + def __init__(self, environment: CodeEnvironment): + super().__init__() + self.type = "CODE" + self.environment = environment + + def __call__(self, code: str, requirements: str): + """ + Execute code with requirements. + """ + return self.execute_code(code, requirements) + + def execute_code(self, code: str, requirements: str): + """ + Execute code with requirements. + + Args: + code (str): The code to be executed. + requirements (str): The command to install additional dependencies. + + Returns: + str: The result of the code execution. + """ + + init_err = self.environment.init_environment(requirements) + if init_err: + # If init error, return error msg + return init_err + + exec_res = self.environment.step(code) + code_str = (f"###############Code###############\n" + f"{code}\n" + f"###############Code###############\n" + f"Code execute result is :{exec_res}") + return code_str, None + + def format_prompt(self): + return { + "name": "code", + "description": CODE_PROMPT + } + + @staticmethod + def display(): + return True diff --git a/pyopenagi/agents/experiment/standard/action/action_tool.py b/pyopenagi/agents/experiment/standard/action/tool.py similarity index 53% rename from pyopenagi/agents/experiment/standard/action/action_tool.py rename to pyopenagi/agents/experiment/standard/action/tool.py index af432bb9d..7869ff3bd 100644 --- a/pyopenagi/agents/experiment/standard/action/action_tool.py +++ b/pyopenagi/agents/experiment/standard/action/tool.py @@ -1,19 +1,53 @@ import importlib -from typing import List, Any, Optional -from pydantic.v1 import BaseModel, root_validator +from typing import Any + from pyopenagi.agents.experiment.standard.action.action import Action from pyopenagi.agents.experiment.standard.utils.config import Config from pyopenagi.agents.experiment.standard.utils.str_utils import snake_to_camel -class ActionTool(Action, BaseModel): +class ActionTool(Action): + """ + Action responsible for support tool call. + """ - config: Config - tools: Optional[List] - tools_format: Optional[List] - type: str = "TOOL" + def __init__(self, config: Config): + super().__init__() + self.tools = {} + self.tools_format = [] + self.type = "TOOL" + self.config = config + self.init_tools() def __call__(self, tool_call: dict) -> Any: + """ + Execute a tool call. + """ + return self.execute(tool_call) + + def execute(self, tool_call: dict) -> Any: + """ + Execute the tool call. + + Args: + tool_call (dict): A dictionary contain function name and parameters. + Example: + { + "name": "function_name", + "parameters": { + "param1": "value1", + "param2": "value2" + } + } + + Returns: + tuple: A tuple of two elements. The first element is the response of the function call, + the second element is the tool call id. + + Raises: + TypeError: If the parameters of the function call is invalid. + Exception: If any other exception occurs. + """ if tool_call is None: return @@ -25,10 +59,11 @@ def __call__(self, tool_call: dict) -> Any: except TypeError: function_response = f"Call function {function_name} failed. Parameters {function_param} is invalid." + except Exception as e: + function_response = f"Tool error is {e}" - return function_response + return function_response, tool_call["id"] - @root_validator(pre=True) def init_tools(self): self._init_tools_from_config() diff --git a/pyopenagi/agents/experiment/standard/agent.py b/pyopenagi/agents/experiment/standard/agent.py index 624a7f333..f24dcaa68 100644 --- a/pyopenagi/agents/experiment/standard/agent.py +++ b/pyopenagi/agents/experiment/standard/agent.py @@ -1,18 +1,20 @@ import time from typing import List -from aios.hooks.request import send_request -from pyopenagi.agents.experiment.standard.action.action_tool import ActionTool +from aios.hooks.syscall import send_request +from pyopenagi.agents.experiment.standard.action.code import ActionCode +from pyopenagi.agents.experiment.standard.action.tool import ActionTool +from pyopenagi.agents.experiment.standard.environment.code_environment import LocalCodeEnvironment from pyopenagi.agents.experiment.standard.memory.short_term_memory import ShortTermMemory from pyopenagi.agents.experiment.standard.planning.planning import Planning, DefaultPlanning from pyopenagi.agents.experiment.standard.prompt.framework_prompt import STANDARD_PROMPT from pyopenagi.agents.experiment.standard.utils.config import load_config -from pyopenagi.utils.chat_template import Query, Response +from pyopenagi.utils.chat_template import LLMQuery, Response from pyopenagi.utils.logger import AgentLogger class StandardAgent: - def __init__(self, agent_name: str, task_input: str, log_mode: str): + def __init__(self, agent_name: str, task_input: str, log_mode: str = "console"): # Init module self.planning: Planning | None = None self.actions = {} @@ -51,10 +53,17 @@ def tools_format(self): else: return None + def _is_terminate(self): + return self.custom_terminate() + + def custom_terminate(self) -> bool: + pass + def custom_prompt(self) -> str: pass def _init_framework_prompt(self): + self.init_module() # Action action_prompt = self._action_prompt() planning_prompt = self._planning_prompt() @@ -62,14 +71,16 @@ def _init_framework_prompt(self): framework_prompt = STANDARD_PROMPT.format( action=action_prompt, planning=planning_prompt, + memory="", + communication="" ) self.short_term_memory.remember(role="system", content=framework_prompt) def _action_prompt(self) -> str: action_prompt = "" - for action in self.actions: - if action.disply: + for action in self.actions.values(): + if action.display: name = action.format_prompt()["name"] description = action.format_prompt()["description"] action_prompt += f"- {name}: {description}\n" @@ -85,10 +96,11 @@ def _planning_prompt(self) -> str: return planning_prompt def init_module(self): - return + self.init_planning() + self.init_actions() - def init_planning(self, planning): - self.planning = planning + def init_planning(self): + self.planning = DefaultPlanning(self.request) def init_communication(self, communication): return @@ -96,62 +108,71 @@ def init_communication(self, communication): def init_memory(self, memory): return - def init_actions(self, actions): - action_tool = ActionTool() - self.actions[action_tool.type] = action_tool - - def planning(self) -> dict: - # Select suitable messages - messages = self.short_term_memory.recall() + def init_actions(self): + # Tool + tool = ActionTool(config=self.config) - planning = DefaultPlanning(self.request) - result = planning(messages, self.tools_format) + # Code + environment = LocalCodeEnvironment() + code = ActionCode(environment=environment) - return result + self.actions[tool.type] = tool + self.actions[code.type] = code def run(self): # Init system prompt and task if custom_prompt := self.custom_prompt(): self.short_term_memory.remember("system", custom_prompt) + self.log_last_message() self.short_term_memory.remember("user", self.task_input) + self.log_last_message() while not self._is_terminate(): # Run planning - planning_result = self.planning() + messages = self.short_term_memory.recall() + planning_result = self.planning(messages, self.tools_format) + if action_type := planning_result.action_type: action = self.actions[action_type] action_param = planning_result.action_param response, tool_call_id = action(**action_param) - self.short_term_memory.remember("assistant", response, tool_call_id) + + if planning_result.text_content: + self.short_term_memory.remember("assistant", planning_result.text_content) + self.log_last_message() + self.short_term_memory.remember("user", response, tool_call_id) + else: response = planning_result.text_content self.short_term_memory.remember("assistant", response) - def _is_terminate(self): - return True if "TERMINATE" in self.short_term_memory.last_message else False + self.log_last_message() + + return { + "agent_name": self.agent_name, + "result": self.short_term_memory.last_message()["content"], + "rounds": self.rounds, + } def request(self, messages: List, tools: List) -> Response: - ( - response, - start_times, - end_times, - waiting_times, - turnaround_times - ) = send_request( + response = send_request( agent_name=self.agent_name, - query=Query( + query=LLMQuery( messages=messages, - tools=tools, - action_type="message_llm", + tools=tools if tools else None, ) ) # Update AIOS monitor info if self.rounds == 0: - self.start_time = start_times[0] + self.start_time = response["start_times"][0] - self.request_waiting_times.extend(waiting_times) - self.request_turnaround_times.extend(turnaround_times) + self.request_waiting_times.extend(response["waiting_times"]) + self.request_turnaround_times.extend(response["turnaround_times"]) self.rounds += 1 - return response + return response["response"] + + def log_last_message(self): + log_content = self.short_term_memory.last_message()["content"] + self.logger.log(f"\n{log_content}\n", "info") diff --git a/pyopenagi/agents/experiment/standard/communication/communication.py b/pyopenagi/agents/experiment/standard/communication/communication.py index e69de29bb..dbe8aa5a0 100644 --- a/pyopenagi/agents/experiment/standard/communication/communication.py +++ b/pyopenagi/agents/experiment/standard/communication/communication.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class Communication(ABC): + + @abstractmethod + def send(self, *args, **kwargs): + pass + + @abstractmethod + def receive(self, *args, **kwargs): + pass diff --git a/pyopenagi/agents/experiment/standard/communication/message_pool.py b/pyopenagi/agents/experiment/standard/communication/message_pool.py new file mode 100644 index 000000000..98e0bad1d --- /dev/null +++ b/pyopenagi/agents/experiment/standard/communication/message_pool.py @@ -0,0 +1,31 @@ +import threading +import queue +from typing import Any + +from pyopenagi.agents.experiment.standard.communication.communication import Communication + +SHARED_DICT: dict[Any, queue] = {} + + +class MessagePool(Communication): + _pool: dict[Any, queue] = SHARED_DICT + + def __init__(self): + super().__init__() + self._lock = threading.Lock() + + async def send(self, message: Any, target: Any) -> None: + with self._lock: + if target not in self._pool: + self._pool[target] = queue.Queue() + self._pool[target].put(message) + + async def receive(self, target: Any) -> Any: + if target not in self._pool: + yield None + + with self._lock: + if target in self._pool: + queue = self._pool[target] + while not queue.empty(): + yield queue.get() diff --git a/pyopenagi/agents/experiment/standard/config.json b/pyopenagi/agents/experiment/standard/config.json index 6f4aba03a..c062b9411 100644 --- a/pyopenagi/agents/experiment/standard/config.json +++ b/pyopenagi/agents/experiment/standard/config.json @@ -4,7 +4,6 @@ ], "tools": [ - ], "meta": { "author": "example", diff --git a/pyopenagi/agents/experiment/standard/environment/__init__.py b/pyopenagi/agents/experiment/standard/environment/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyopenagi/agents/experiment/standard/environment/code_environment.py b/pyopenagi/agents/experiment/standard/environment/code_environment.py new file mode 100644 index 000000000..6f13d6421 --- /dev/null +++ b/pyopenagi/agents/experiment/standard/environment/code_environment.py @@ -0,0 +1,88 @@ +import subprocess +import sys +import tempfile +from abc import ABC +from typing import List + +from pyopenagi.agents.experiment.standard.environment.environment import Environment + + +class CodeEnvironment(Environment, ABC): + """ + Environment for code. + """ + + +class LocalCodeEnvironment(CodeEnvironment): + """ + Local code environment. + """ + + def init_environment(self, requirement_list: List[str]): + """ + Initialize the environment with requirements. + + Args: + requirement_list (List[str]): The list of requirement commands(Format like `pip install numpy`). + + """ + if requirement_list is None: + return + + for requirement in requirement_list: + if "pip install" in requirement: + package = requirement.split(" ")[-1] + else: + package = requirement + try: + subprocess.run([sys.executable, "-m", "pip", "install", package], check=True) + print(f"Run command successfully: `pip install {package}`") + except subprocess.CalledProcessError as e: + err_msg = (f"Run command failed: `pip install {package}`\n" + f"Return code: {e.returncode}\n" + f"Error message: {e.stderr}\n" + f"Output: {e.stdout}") + print(err_msg) + return err_msg + + def step(self, code_block: str, language: str = "python"): + """ + Execute the given code block in a temporary file. + + Args: + code_block (str): The code block to be executed. + language (str, optional): The language of the code block. Defaults to "python". + + Returns: + str: The result of the code execution. + """ + # Create temp file, write python code into temp file, then execute it + with tempfile.NamedTemporaryFile(mode="w+", suffix=".py", delete=True) as temp_file: + temp_file.write(code_block) + temp_file.flush() + try: + exec_res = subprocess.run( + ["python", temp_file.name], + capture_output=True, + text=True, + check=True) + step_res = exec_res.stdout + except subprocess.CalledProcessError as e: + err_msg = (f"Run python code failed:`\n" + f"Return code: {e.returncode}\n" + f"Error message: {e.stderr}\n" + f"Output: {e.stdout}") + step_res = err_msg + return step_res + + +class DockerCodeEnvironment(CodeEnvironment): + """ + Docker code environment. + """ + + def init_environment(self, *args, **kwargs): + pass + + def step(self, *args, **kwargs): + pass diff --git a/pyopenagi/agents/experiment/standard/environment/environment.py b/pyopenagi/agents/experiment/standard/environment/environment.py new file mode 100644 index 000000000..344d13b8c --- /dev/null +++ b/pyopenagi/agents/experiment/standard/environment/environment.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod + + +class Environment(ABC): + """ + Environment abstract base class. The environment is the foundation for the agent to execute actions. + """ + + @abstractmethod + def init_environment(self, *args, **kwargs): + """Init environment, prepare something required""" + pass + + @abstractmethod + def step(self, *args, **kwargs): + """Take a step in the environment""" + pass diff --git a/pyopenagi/agents/experiment/standard/memory/aios_memory.py b/pyopenagi/agents/experiment/standard/memory/aios_memory.py new file mode 100644 index 000000000..66fe83ce8 --- /dev/null +++ b/pyopenagi/agents/experiment/standard/memory/aios_memory.py @@ -0,0 +1,19 @@ +from pyopenagi.agents.experiment.standard.memory.memory import Memory + + +class AIOSMemory(Memory): + + def add(self, *args, **kwargs): + pass + + def query(self, *args, **kwargs): + pass + + def update(self, *args, **kwargs): + pass + + def delete(self, *args, **kwargs): + pass + + def compress(self, *args, **kwargs): + pass diff --git a/pyopenagi/agents/experiment/standard/memory/memory.py b/pyopenagi/agents/experiment/standard/memory/memory.py index 9469f855b..aa9bc23d7 100644 --- a/pyopenagi/agents/experiment/standard/memory/memory.py +++ b/pyopenagi/agents/experiment/standard/memory/memory.py @@ -1,2 +1,24 @@ -class MemoryStandard: - ... +from abc import ABC, abstractmethod + + +class Memory(ABC): + + @abstractmethod + def add(self, *args, **kwargs): + pass + + @abstractmethod + def query(self, *args, **kwargs): + pass + + @abstractmethod + def update(self, *args, **kwargs): + pass + + @abstractmethod + def delete(self, *args, **kwargs): + pass + + @abstractmethod + def compress(self, *args, **kwargs): + pass diff --git a/pyopenagi/agents/experiment/standard/memory/short_term_memory.py b/pyopenagi/agents/experiment/standard/memory/short_term_memory.py index db4be8ac8..820f86001 100644 --- a/pyopenagi/agents/experiment/standard/memory/short_term_memory.py +++ b/pyopenagi/agents/experiment/standard/memory/short_term_memory.py @@ -8,10 +8,10 @@ class ShortTermMemory(BaseModel): def remember(self, role: str, content: str, tool_call_id: int = None) -> None: if tool_call_id: message = {"role": role, "content": content, "tool_call_id": tool_call_id} - self.messages += message + self.messages.append(message) else: message = {"role": role, "content": content} - self.messages += message + self.messages.append(message) def recall(self): return self.messages diff --git a/pyopenagi/agents/experiment/standard/planning/memory_augmented.py b/pyopenagi/agents/experiment/standard/planning/memory_augmented.py new file mode 100644 index 000000000..dfbde8911 --- /dev/null +++ b/pyopenagi/agents/experiment/standard/planning/memory_augmented.py @@ -0,0 +1,10 @@ +from pyopenagi.agents.experiment.standard.planning.planning import Planning + + +class PlanningMemoryAugmented(Planning): + + def __call__(self, *args, **kwargs): + pass + + def format_prompt(self): + pass diff --git a/pyopenagi/agents/experiment/standard/planning/planning.py b/pyopenagi/agents/experiment/standard/planning/planning.py index c1fad557f..2abc6729a 100644 --- a/pyopenagi/agents/experiment/standard/planning/planning.py +++ b/pyopenagi/agents/experiment/standard/planning/planning.py @@ -1,3 +1,4 @@ +import re from abc import ABC, abstractmethod from typing import List, Callable, Optional, Any @@ -30,20 +31,46 @@ def __init__(self, request_func: Callable[[List, List], Response]): def __call__(self, messages: List, tools: List): response = self.request_func(messages, tools) - - response_message = response.response_message - tool_calls = response.tool_calls + if tool_calls := response.tool_calls: + response_message = None + else: + response_message = response.response_message result = PlanningResult() result.text_content = response_message + if tool_calls: + # action tool result.action_type = "TOOL" - result.action_param = tool_calls[0] + result.action_param = { + "tool_call": tool_calls[0] + } + return result + + code_info = extract_code(response_message) + if not all(info is None for info in code_info): + # action code + result.action_type = "CODE" + result.action_param = { + "code": code_info[0], + "requirements": code_info[1], + } + return result return result def format_prompt(self): return { "name": "normal", - "description": "Planning as normal." + "description": "Try to solve promble step by step. Think more before you try to give final answer." } + + +def extract_code(message) -> tuple[str, str] | None: + """extract code from message""" + code_match = re.search(r'```python\s*([\s\S]*?)```', message) + requirements_match = re.search(r'```requirements\s*([\s\S]*?)```', message) + + code = code_match.group(1) if code_match else None + requirements = requirements_match.group(1) if requirements_match else None + return code, requirements diff --git a/pyopenagi/agents/experiment/standard/planning/reflexion.py b/pyopenagi/agents/experiment/standard/planning/reflexion.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyopenagi/agents/experiment/standard/prompt/framework_prompt.py b/pyopenagi/agents/experiment/standard/prompt/framework_prompt.py index 8b8290606..db2eb968c 100644 --- a/pyopenagi/agents/experiment/standard/prompt/framework_prompt.py +++ b/pyopenagi/agents/experiment/standard/prompt/framework_prompt.py @@ -11,20 +11,19 @@ The following will explain the functions of the modules and how to use them. - -{action} - - -{planning} + {planning} + + {action} + + -{memory} + {memory} -{communication} + {communication} - """ diff --git a/pyopenagi/tools/arxiv/arxiv.py b/pyopenagi/tools/arxiv/arxiv.py index 4d6785c12..258f3a58c 100644 --- a/pyopenagi/tools/arxiv/arxiv.py +++ b/pyopenagi/tools/arxiv/arxiv.py @@ -101,7 +101,7 @@ def get_tool_call_format(self): tool_call_format = { "type": "function", "function": { - "name": "arxiv/arxiv", + "name": "arxiv", "description": "Query articles or topics in arxiv", "parameters": { "type": "object", diff --git a/pyopenagi/tools/reader/file_reader.py b/pyopenagi/tools/reader/file_reader.py new file mode 100644 index 000000000..9b0a96259 --- /dev/null +++ b/pyopenagi/tools/reader/file_reader.py @@ -0,0 +1,340 @@ +import base64 +import csv +import json +import os +import zipfile +from abc import ABC, abstractmethod + +import PyPDF2 +import pandas as pd +import pptx +import docx + +from openai import OpenAI + +from pyopenagi.tools.base import BaseTool + + +class FileReader(BaseTool): + + def __init__(self): + super().__init__() + + def run(self, param) -> str: + # query = param["query"] # Temporarily unused + path = param["path"] + + reader = get_reader(path) + content = reader.read(path) + return content + + def get_tool_call_format(self): + tool_call_format = { + "type": "function", + "function": { + "name": "file_reader", + "description": "Read the file content from the specified path.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Relevant infromation want to retrival. If want to extract all file " + "content, don't pass `query`." + }, + "path": { + "type": "string", + "description": "Input file path." + } + }, + "required": [ + "path" + ] + } + } + } + return tool_call_format + + +READER_REGISTER = {} + + +def register_reader(*args): + """ + A decorator to register a reader class into the READER_REGISTER dictionary. + + Usage: + @register_reader(".pdf", ".docx") + class PDFReader(Reader): + # implement the read method + pass + + In this example, the PDFReader class is registered to handle files with ".pdf" + and ".docx" extensions. + """ + def decorator(cls): + for suffix in args: + READER_REGISTER[suffix] = cls + return cls + + return decorator + + +class Reader(ABC): + + @abstractmethod + def read(self, path: str) -> str: + pass + + +def get_reader(path: str) -> Reader: + """ + Retrieves a reader instance based on the file extension of the given path. + + Args: + path (str): The file path for which to get the corresponding reader. + + Returns: + Reader: An instance of a reader class that can handle the file extension + of the given path. This instance is expected to implement the `read` method + to process the file content. + """ + filename, file_extension = os.path.splitext(path) + reader = READER_REGISTER.get(file_extension) + return reader() + + +@register_reader(".jpg", ".png") +class ImageReader(Reader): + """ + Reads image files and recognizes their content using the OpenAI API. + """ + + def __init__(self): + self.client = OpenAI() + + def read(self, path: str) -> str: + with open(path, "rb") as image_file: + base64_img = base64.b64encode(image_file.read()).decode("utf-8") + + response = self.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this Image?"}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_img}", + }, + }, + ], + } + ], + max_tokens=300, + ) + return response.choices[0].message.content + + +@register_reader(".pdf") +class PDFReader(Reader): + """ + Reads PDF files and returns the content of each page as plain text. + """ + + def read(self, path: str) -> str: + content = "" + pdf = PyPDF2.PdfReader(path) + for index, page in enumerate(pdf.pages): + content += f"Page {index + 1}:\n" + page.extract_text() + return content + + +@register_reader(".pptx") +class PPTReader(Reader): + """ + Reads PowerPoint files (.pptx) and returns the content of each slide as plain text. + + The content of each slide is concatenated and returned as a single string. + + Note: This reader does not support PPT files that contain images or other + non-text content. + """ + + def read(self, path: str) -> str: + content = "" + ppt = pptx.Presentation(path) + for index, slide in enumerate(ppt.slides): + content += f"Slide {index + 1}:\n" + for shape in slide.shapes: + if hasattr(shape, "text"): + content += shape.text + return content + + +@register_reader(".txt", ".pdb") +class TextReader(Reader): + """ + Reads plain text files (.txt and .pdb) and returns the content as a string. + + The content of the file is read and returned as a single string. + """ + + def read(self, path: str) -> str: + content = "" + with open(path, "r", encoding="utf-8") as file: + content += file.read() + return content + + +@register_reader(".mp3") +class AudioReader(Reader): + """ + Reads audio files (.mp3) and transcribes the content using the Whisper API. + + This reader uses the Whisper API to transcribe the audio file and returns the + transcription as a string. + + Note: This reader requires an OpenAI API key to be set in the environment. + """ + + def __init__(self): + self.client = OpenAI() + + def read(self, path: str) -> str: + content = "" + with open(path, "rb") as audio_file: + response = self.client.audio.transcriptions.create( + model="whisper-1", + file=audio_file + ) + content += response.text + + return content + + +@register_reader(".xlsx") +class ExcelReader(Reader): + """ + Reads Excel files (.xlsx) and returns the content as a string. + + The content of the file is read into a pandas DataFrame and then converted + to a string using the `to_string` method. + + Note: This reader requires the `pandas` library to be installed. + """ + + def read(self, path: str) -> str: + content = "" + data = pd.read_excel(path) + content += data.to_string() + return content + + +@register_reader(".json", "jsonl") +class JsonReader(Reader): + """ + Reads JSON and JSONL files and returns their content as a string. + + For JSON files, the content is loaded as a dictionary and converted to a string. + For JSONL files, each line is loaded as a JSON object and the list of these + objects is converted to a string. + """ + + def read(self, path): + content = "" + with open(path, "r", encoding="utf-8") as file: + if path.endswith(".json"): + content += str(json.load(file)) + elif path.endswith(".jsonl"): + content += str([json.loads(line) for line in file]) + return content + + +@register_reader(".docx") +class DocxReader(Reader): + """ + Reads DOCX files and returns the content as plain text. + + This reader processes each paragraph in the DOCX file, appending + it to the content string with a page indicator. + """ + + def read(self, path: str) -> str: + content = "" + doc = docx.Document(path) + for index, page in enumerate(doc.paragraphs): + content += f"Page {index + 1}:\n" + page.text + "\n" + return content + + +@register_reader(".py") +class PythonReader(Reader): + """ + Reads Python files (.py) and returns their content as a string. + + The content of the file is read and returned as a single string. + """ + + def read(self, path: str) -> str: + content = "" + with open(path, "r", encoding="utf-8") as file: + content += file.read() + return content + + +@register_reader(".zip") +class ZipReader(Reader): + """ + Reads the content of a ZIP file. + + This reader will extract the ZIP file to the same directory and then + read the content of each file in the ZIP file. The content of each file + is processed by the corresponding reader and the results are concatenated + together with each file's name and content separated by a blank line. + """ + + def read(self, path: str) -> str: + content = "" + with zipfile.ZipFile(path, "r") as zip_file: + extract_dir = path[:-4] + '/' + zip_file.extractall(extract_dir) + for file_name in zip_file.namelist(): + content += f"File {file_name}:\n" + + sub_reader = get_reader(extract_dir + file_name) + content += sub_reader.read(extract_dir + file_name) + "\n" + + return content + + +@register_reader(".csv") +class CSVReader(Reader): + """ + Reads CSV files and returns their content as a string. + + The content of the CSV file is read into a list of rows, where each row is a list of values. + The list is then converted to a string and returned. + """ + + def read(self, path: str) -> str: + content = "" + with open(path, newline='', encoding="utf-8") as file: + reader = csv.reader(file) + data = [row for row in reader] + content += str(data) + return content + + +if __name__ == "__main__": + param = { + "path": "" + } + reader = FileReader() + content = reader.run(param) + print(f"File Path: {param['path']}\n" + f"\n " + f"{content}\n " + f"") diff --git a/pyopenagi/utils/chat_template.py b/pyopenagi/utils/chat_template.py index 7c2c749ca..df2550f82 100755 --- a/pyopenagi/utils/chat_template.py +++ b/pyopenagi/utils/chat_template.py @@ -8,11 +8,11 @@ class Request(BaseModel): class LLMQuery(Request): """ Query class represents the input structure for performing various actions. - + Attributes: messages (List[Dict[str, Union[str, Any]]]): A list of dictionaries where each dictionary represents a message containing 'role' and 'content' or other key-value pairs. - tools (Optional[List[Dict[str, Any]]]): An optional list of JSON-like objects (dictionaries) + tools (Optional[List[Dict[str, Any]]]): An optional list of JSON-like objects (dictionaries) representing tools and their parameters. Default is an empty list. action_type (Literal): A string that must be one of "message_llm", "call_tool", or "operate_file". This restricts the type of action the query performs. @@ -25,7 +25,7 @@ class LLMQuery(Request): class Config: arbitrary_types_allowed = True # Allows the use of arbitrary types such as Any and Dict. - + class MemoryQuery(Request): # messages: List[Dict[str, Union[str, Any]]] # List of message dictionaries, each containing role and content. @@ -35,7 +35,7 @@ class MemoryQuery(Request): class Config: arbitrary_types_allowed = True # Allows the use of arbitrary types such as Any and Dict. - + class StorageQuery(Request): messages: List[Dict[str, Union[str, Any]]] # List of message dictionaries, each containing role and content. @@ -54,10 +54,10 @@ class Config: class Response(BaseModel): """ Response class represents the output structure after performing actions. - + Attributes: response_message (Optional[str]): The generated response message. Default is None. - tool_calls (Optional[List[Dict[str, Any]]]): An optional list of JSON-like objects (dictionaries) + tool_calls (Optional[List[Dict[str, Any]]]): An optional list of JSON-like objects (dictionaries) representing the tool calls made during processing. Default is None. """ response_message: Optional[str] = None # The generated response message, default is None. diff --git a/requirements.txt b/requirements.txt index 3cca9f0cf..3c30334a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,9 @@ platformdirs arxiv llama-index-embeddings-huggingface watchdog -chromadb \ No newline at end of file +chromadb + +# tool +PyPDF2 +python-pptx +python-docx diff --git a/scripts/aios-interpreter/example_aios_interpreter.py b/scripts/aios-interpreter/example_aios_interpreter.py index 0a5487792..1ae7af1a5 100644 --- a/scripts/aios-interpreter/example_aios_interpreter.py +++ b/scripts/aios-interpreter/example_aios_interpreter.py @@ -3,7 +3,7 @@ from aios.sdk import FrameworkType from aios.sdk.adapter import prepare_framework -from aios.hooks.llm import aios_starter +from aios.hooks.starter import aios_starter from aios.utils.utils import ( parse_global_args, delete_directories diff --git a/scripts/aios-openagi/write_code.py b/scripts/aios-openagi/write_code.py new file mode 100644 index 000000000..ad9f1c0c3 --- /dev/null +++ b/scripts/aios-openagi/write_code.py @@ -0,0 +1,42 @@ +import os +import warnings + +from dotenv import load_dotenv + +from aios.hooks.starter import aios_starter +from aios.utils.utils import parse_global_args +from pyopenagi.agents.experiment.standard.agent import StandardAgent + + +class StandardAgentImpl(StandardAgent): + + def custom_prompt(self) -> str: + return "If you think task is finished, output TERMINATE." + + def custom_terminate(self) -> bool: + return True if "TERMINATE" in self.short_term_memory.last_message()["content"] else False + + +def main(): + main_id = os.getpid() + print(f"Main ID is: {main_id}") + warnings.filterwarnings("ignore") + parser = parse_global_args() + args = parser.parse_args() + load_dotenv() + + with aios_starter(**vars(args)): + agent = StandardAgentImpl( + agent_name="Agent", + task_input="Write code to solve following problem: In a group of 23 people, the probability of at least " + "two having the same birthday is greater" + "than 50%" + ) + + result = agent.run() + print(f"Result: {result}") + + +# python -m scripts.aios-openagi.write_code --llm_name gpt-4o-mini +if __name__ == '__main__': + main() diff --git a/scripts/aios-seeact/run_seeact.py b/scripts/aios-seeact/run_seeact.py index b051bb9df..25e4275fd 100644 --- a/scripts/aios-seeact/run_seeact.py +++ b/scripts/aios-seeact/run_seeact.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 import os import sys -# Add AIOS path -aios_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) -sys.path.insert(0, aios_root) +import warnings +import asyncio from aios.utils.utils import ( parse_global_args, delete_directories ) -import warnings from aios.hooks.starter import aios_starter from dotenv import load_dotenv -import asyncio + +# Add AIOS path +aios_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.insert(0, aios_root) + def clean_cache(root_directory): targets = { @@ -22,6 +24,7 @@ def clean_cache(root_directory): } delete_directories(root_directory, targets) + def main(): main_id = os.getpid() print(f"Main ID is: {main_id}") @@ -67,5 +70,6 @@ def main(): clean_cache(root_directory="./") + if __name__ == "__main__": - main() \ No newline at end of file + main()