diff --git a/agenthub/app/agentchat/page.tsx b/agenthub/app/agentchat/page.tsx index 501772f2..070229ff 100644 --- a/agenthub/app/agentchat/page.tsx +++ b/agenthub/app/agentchat/page.tsx @@ -11,6 +11,7 @@ import { MessageList } from '@/components/agentchat/MessageList'; import axios from 'axios'; import { AgentCommand } from '@/components/chat/body/message-box'; import { baseUrl, serverUrl } from '@/lib/env'; +import { generateSixDigitId } from '@/lib/utils'; @@ -94,7 +95,7 @@ const ChatInterface: React.FC = () => { const handleSend = async (content: string, attachments: File[]) => { if (content.trim() || attachments.length > 0) { const newMessage: Message = { - id: Date.now(), + id: `${generateSixDigitId()}`, text: content, sender: 'user', timestamp: new Date(), @@ -103,7 +104,7 @@ const ChatInterface: React.FC = () => { }; setMessages([...messages, newMessage]); - let messageId = Date.now(); + let messageId = generateSixDigitId(); // Handle file uploads here (e.g., to a server) const botMessage: Message = { @@ -120,11 +121,13 @@ const ChatInterface: React.FC = () => { setMessages(prevMessages => [...prevMessages].map(message => { if (message.id == messageId) { - return { ...message, thinking: false }; + return { ...message, thinking: false, text: res.content }; } - return res.content; + // return res.content; + return message; })); } + }; const addChat = () => { diff --git a/agenthub/lib/env.ts b/agenthub/lib/env.ts index 010f719e..1376dba9 100644 --- a/agenthub/lib/env.ts +++ b/agenthub/lib/env.ts @@ -1,10 +1,6 @@ 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 = process.env.NODE_ENV === 'development' - ? 'http://localhost:3000' - : 'https://my.aios.foundation'; +export const baseUrl = inDevEnvironment ? 'http://localhost:3000' : 'https://my.aios.foundation' // export const serverUrl = inDevEnvironment ? 'http://localhost:8000' : 'http://35.232.56.61:8000' -export const serverUrl = process.env.NODE_ENV === 'development' - ? 'http://localhost:8000' - : 'https://api.aios.chat'; +export const serverUrl = 'http://35.232.56.61:8000'; diff --git a/cerebrum/__init__.py b/cerebrum/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/agents/base.py b/cerebrum/agents/base.py new file mode 100644 index 00000000..382db7ff --- /dev/null +++ b/cerebrum/agents/base.py @@ -0,0 +1,150 @@ +import os +import json +import time + +from cerebrum.utils.chat import Query +from cerebrum.runtime.process import AgentProcessor +import importlib + +class BaseAgent: + def __init__(self, + agent_name: str, + task_input: str, + config: dict): + # super().__init__() + self.agent_name = agent_name + self.config = config + + self.tool_list = dict() + self.tools = [] + self.tool_info = ([]) # simplified information of the tool: {"name": "xxx", "description": "xxx"} + + self.load_tools(self.config.get('tools')) + self.rounds = 0 + + self.task_input = task_input + self.messages = [] + self.workflow_mode = "manual" # (manual, automatic) + + self.llm = None + + + def run(self): + # raise NotImplementedError + pass + + def build_system_instruction(self): + pass + + + def check_workflow(self, message): + try: + # print(f"Workflow message: {message}") + workflow = json.loads(message) + if not isinstance(workflow, list): + return None + + for step in workflow: + if "message" not in step or "tool_use" not in step: + return None + + return workflow + + except json.JSONDecodeError: + return None + + def automatic_workflow(self): + for i in range(self.plan_max_fail_times): + response = AgentProcessor.process_response(query=Query(messages=self.messages, tools=None, message_return_type="json"), llm=self.llm) + + workflow = self.check_workflow(response.response_message) + + self.rounds += 1 + + if workflow: + return workflow + + else: + self.messages.append( + { + "role": "assistant", + "content": f"Fail {i+1} times to generate a valid plan. I need to regenerate a plan", + } + ) + return None + + def manual_workflow(self): + pass + + def check_path(self, tool_calls): + script_path = os.path.abspath(__file__) + save_dir = os.path.join( + os.path.dirname(script_path), "output" + ) # modify the customized output path for saving outputs + if not os.path.exists(save_dir): + os.makedirs(save_dir) + for tool_call in tool_calls: + try: + for k in tool_call["parameters"]: + if "path" in k: + path = tool_call["parameters"][k] + if not path.startswith(save_dir): + tool_call["parameters"][k] = os.path.join( + save_dir, os.path.basename(path) + ) + except Exception: + continue + return tool_calls + + def snake_to_camel(self, snake_str): + components = snake_str.split("_") + return "".join(x.title() for x in components) + + def load_tools(self, tool_names): + if tool_names == None: + return + + for tool_name in tool_names: + org, name = tool_name.split("/") + module_name = ".".join(["cerebrum", "tools", org, name]) + class_name = self.snake_to_camel(name) + tool_module = importlib.import_module(module_name) + tool_class = getattr(tool_module, class_name) + self.tool_list[name] = tool_class() + tool_format = tool_class().get_tool_call_format() + self.tools.append(tool_format) + self.tool_info.append( + { + "name": tool_format["function"]["name"], + "description": tool_format["function"]["description"], + } + ) + + def pre_select_tools(self, tool_names): + pre_selected_tools = [] + for tool_name in tool_names: + for tool in self.tools: + if tool["function"]["name"] == tool_name: + pre_selected_tools.append(tool) + break + + return pre_selected_tools + + def create_agent_request(self, query): + agent_process = self.agent_process_factory.activate_agent_process( + agent_name=self.agent_name, query=query + ) + agent_process.set_created_time(time.time()) + # print("Already put into the queue") + return agent_process + + def set_aid(self, aid): + self.aid = aid + + def get_aid(self): + return self.aid + + def get_agent_name(self): + return self.agent_name + + diff --git a/cerebrum/agents/react.py b/cerebrum/agents/react.py new file mode 100644 index 00000000..3bd317c5 --- /dev/null +++ b/cerebrum/agents/react.py @@ -0,0 +1,198 @@ +from cerebrum.agents.base import BaseAgent +from cerebrum.runtime.process import AgentProcessor +from cerebrum.utils.chat import Query + +import time +import json + +class ReactAgent(BaseAgent): + def __init__(self, agent_name: str, task_input: str, config: dict): + BaseAgent.__init__(self, agent_name, task_input, config) + + self.plan_max_fail_times = 3 + self.tool_call_max_fail_times = 3 + + self.llm = None + + + def build_system_instruction(self): + prefix = "".join( + [ + "".join(self.config.get('description', 'You are an AI agent.')) + ] + ) + + plan_instruction = "".join( + [ + f'You are given the available tools from the tool list: {json.dumps(self.tool_info)} to help you solve problems. ', + 'Generate a plan of steps you need to take. ', + 'The plan must follow the json format as below: ', + '[', + '{"message": "message_value1","tool_use": [tool_name1, tool_name2,...]}', + '{"message": "message_value2", "tool_use": [tool_name1, tool_name2,...]}', + '...', + ']', + 'In each step of the planned plan, identify tools to use and recognize no tool is necessary. ', + 'Followings are some plan examples. ', + '[' + '[', + '{"message": "gather information from arxiv. ", "tool_use": ["arxiv"]},', + '{"message", "write a summarization based on the gathered information. ", "tool_use": []}', + '];', + '[', + '{"message": "gather information from arxiv. ", "tool_use": ["arxiv"]},', + '{"message", "understand the current methods and propose ideas that can improve ", "tool_use": []}', + ']', + ']' + ] + ) + + if self.workflow_mode == "manual": + self.messages.append( + {"role": "system", "content": prefix} + ) + + else: + assert self.workflow_mode == "automatic" + self.messages.append( + {"role": "system", "content": prefix + plan_instruction} + ) + + + def automatic_workflow(self): + return super().automatic_workflow() + + def manual_workflow(self): + pass + + def call_tools(self, tool_calls): + # self.logger.log(f"***** It starts to call external tools *****\n", level="info") + success = True + actions = [] + observations = [] + + # print(tool_calls) + for tool_call in tool_calls: + # print(tool_call) + function_name = tool_call["name"] + function_to_call = self.tool_list[function_name] + function_params = tool_call["parameters"] + + try: + function_response = function_to_call.run(function_params) + actions.append(f"I will call the {function_name} with the params as {function_params}") + observations.append(f"The output of calling the {function_name} tool is: {function_response}") + + except Exception: + actions.append("I fail to call any tools.") + observations.append(f"The tool parameter {function_params} is invalid.") + success = False + + return actions, observations, success + + def run(self): + super().run() + self.build_system_instruction() + + task_input = self.task_input + + self.messages.append( + {"role": "user", "content": task_input} + ) + + workflow = None + + if self.workflow_mode == "automatic": + workflow = self.automatic_workflow() + else: + assert self.workflow_mode == "manual" + workflow = self.manual_workflow() + + self.messages.append( + {"role": "assistant", "content": f"[Thinking]: The workflow generated for the problem is {json.dumps(workflow)}"} + ) + + self.messages.append( + {"role": "user", "content": "[Thinking]: Follow the workflow to solve the problem step by step. "} + ) + + # if workflow: + # print(f"Generated workflow is: {workflow}\n", level="info") + # else: + # print("Fail to generate a valid workflow. Invalid JSON?\n", level="info") + + try: + if workflow: + final_result = "" + + for i, step in enumerate(workflow): + message = step["message"] + tool_use = step["tool_use"] + + prompt = f"At step {i + 1}, you need to: {message}. Outputs should be pure text without any json object" + self.messages.append({ + "role": "user", + "content": prompt + }) + if tool_use: + selected_tools = self.pre_select_tools(tool_use) + + else: + selected_tools = None + + print(Query(messages=self.messages, tools=None, message_return_type="json")) + + response = AgentProcessor.process_response(query=Query(messages=self.messages, tools=None, message_return_type="json"), llm=self.llm) + + # execute action + response_message = response.response_message + + tool_calls = response.tool_calls + + + if tool_calls: + for _ in range(self.plan_max_fail_times): + tool_calls = self.check_path(tool_calls) + actions, observations, success = self.call_tools(tool_calls=tool_calls) + + action_messages = "[Action]: " + ";".join(actions) + observation_messages = "[Observation]: " + ";".join(observations) + + self.messages.append( + { + "role": "assistant", + "content": action_messages + ". " + observation_messages + } + ) + if success: + break + else: + thinkings = response_message + self.messages.append({ + "role": "assistant", + "content": thinkings + }) + + if i == len(workflow) - 1: + final_result = self.messages[-1] + + step_result = self.messages[-1]["content"] + # print(f"At step {i + 1}, {step_result}\n", level="info") + + self.rounds += 1 + + return { + "agent_name": self.agent_name, + "result": final_result, + "rounds": self.rounds, + } + + else: + return { + "agent_name": self.agent_name, + "result": "Failed to generate a valid workflow in the given times.", + "rounds": self.rounds, + } + except Exception as e: + print(e) + return {} \ No newline at end of file diff --git a/cerebrum/interface/__init__.py b/cerebrum/interface/__init__.py new file mode 100644 index 00000000..1d63a2b1 --- /dev/null +++ b/cerebrum/interface/__init__.py @@ -0,0 +1,47 @@ +from cerebrum.manager.manager import AgentManager +from cerebrum.llm.adapter import LLMAdapter +from cerebrum.runtime.process import RunnableAgent + +class AutoAgent: + MANAGER = AgentManager('https://my.aios.foundation') + + @classmethod + def from_pretrained(cls, name: str): + _author, _name = name.split('/') + + _author, _name, _version = cls.MANAGER.download_agent(_author, _name) + + agent, config = cls.MANAGER.load_agent( + _author, + _name, + _version + ) + + return agent, config + + +class AutoLLM: + + @classmethod + def from_foundational(cls, name: str): + _llm_factory = LLMAdapter(name) + + _llm = _llm_factory.get_model() + + return _llm + + +class AutoAgentGenerator: + + @classmethod + def build_agent(cls, agent_name: str, llm_name: str): + agent, config = AutoAgent.from_pretrained(agent_name) + llm = AutoLLM.from_foundational(llm_name) + + _agent = RunnableAgent(agent, config, llm) + + print(config) + + # _agent.llm = llm + + return _agent \ No newline at end of file diff --git a/cerebrum/llm/adapter.py b/cerebrum/llm/adapter.py new file mode 100644 index 00000000..36d85173 --- /dev/null +++ b/cerebrum/llm/adapter.py @@ -0,0 +1,83 @@ + +from cerebrum.llm.base import BaseLLM +from cerebrum.llm.registry import API_MODEL_REGISTRY +# from .llm_classes.hf_native_llm import HfNativeLLM + +# standard implementation of LLM methods +# from .llm_classes.ollama_llm import OllamaLLM +# from .llm_classes.vllm import vLLM + +class LLMAdapter: + """Parameters for LLMs + + Args: + llm_name (str): Name of the LLMs + max_gpu_memory (dict, optional): Maximum GPU resources that can be allocated to the LLM. Defaults to None. + eval_device (str, optional): Evaluation device of binding LLM to designated devices for inference. Defaults to None. + max_new_tokens (int, optional): Maximum token length generated by the LLM. Defaults to 256. + log_mode (str, optional): Mode of logging the LLM processing status. Defaults to "console". + use_backend (str, optional): Backend to use for speeding up open-source LLMs. Defaults to None. Choices are ["vllm", "ollama"] + """ + + def __init__(self, + llm_name: str, + max_gpu_memory: dict = None, + eval_device: str = None, + max_new_tokens: int = 256, + use_backend: str = None + ): + + self.model: BaseLLM | None = None + + # For API-based LLM + if llm_name in API_MODEL_REGISTRY.keys(): + self.model = API_MODEL_REGISTRY[llm_name]( + llm_name = llm_name, + ) + # For locally-deployed LLM + else: + if use_backend == "ollama" or llm_name.startswith("ollama"): + # self.model = OllamaLLM( + # llm_name=llm_name, + # max_gpu_memory=max_gpu_memory, + # eval_device=eval_device, + # max_new_tokens=max_new_tokens, + # log_mode=log_mode + # ) + pass + + elif use_backend == "vllm": + # self.model = vLLM( + # llm_name=llm_name, + # max_gpu_memory=max_gpu_memory, + # eval_device=eval_device, + # max_new_tokens=max_new_tokens, + # log_mode=log_mode + # ) + pass + else: # use huggingface LLM without backend + # self.model = HfNativeLLM( + # llm_name=llm_name, + # max_gpu_memory=max_gpu_memory, + # eval_device=eval_device, + # max_new_tokens=max_new_tokens, + # log_mode=log_mode + # ) + pass + + # def execute(self, + # agent_process, + # temperature=0.0) -> None: + # """Address request sent from the agent + + # Args: + # agent_process: AgentProcess object that contains request sent from the agent + # temperature (float, optional): Parameter to control the randomness of LLM output. Defaults to 0.0. + # """ + # self.model.execute(agent_process,temperature) + + def get_model(self) -> BaseLLM | None: + return self.model + + + \ No newline at end of file diff --git a/cerebrum/llm/base.py b/cerebrum/llm/base.py new file mode 100644 index 00000000..d13f22bc --- /dev/null +++ b/cerebrum/llm/base.py @@ -0,0 +1,120 @@ +import json +import re + +# abc allows to make abstract classes +from abc import ABC, abstractmethod + + +from cerebrum.utils.chat import Query +from cerebrum.utils.llm import generator_tool_call_id + + +class BaseLLM(ABC): + def __init__(self, + llm_name: str, + max_gpu_memory: dict = None, + eval_device: str = None, + max_new_tokens: int = 256, + ): + self.max_gpu_memory = max_gpu_memory + self.eval_device = eval_device + self.max_new_tokens = max_new_tokens + + self.model_name = llm_name + + self.load_llm_and_tokenizer() + + + + def convert_map(self, map: dict) -> dict: + """ helper utility to convert the keys of a map to int """ + new_map = {} + for k, v in map.items(): + new_map[int(k)] = v + return new_map + + def check_model_type(self, model_name): + # TODO add more model types + return "causal_lm" + + + @abstractmethod + def load_llm_and_tokenizer(self) -> None: # load model from config + # raise NotImplementedError + """Load model and tokenizers for each type of LLMs + """ + return + + # only use for open-sourced LLM + def tool_calling_input_format(self, messages: list, tools: list) -> list: + """Integrate tool information into the messages for open-sourced LLMs + + Args: + messages (list): messages with different roles + tools (list): tool information + """ + prefix_prompt = "In and only in current step, you need to call tools. Available tools are: " + tool_prompt = json.dumps(tools) + suffix_prompt = "".join( + [ + 'Must call functions that are available. To call a function, respond ' + 'immediately and only with a list of JSON object of the following format:' + '{[{"name":"function_name_value","parameters":{"parameter_name1":"parameter_value1",' + '"parameter_name2":"parameter_value2"}}]}' + ] + ) + + # translate tool call message for models don't support tool call + for message in messages: + if "tool_calls" in message: + message["content"] = json.dumps(message.pop("tool_calls")) + elif message["role"] == "tool": + message["role"] = "user" + tool_call_id = message.pop("tool_call_id") + content = message.pop("content") + message["content"] = f"The result of the execution of function(id :{tool_call_id}) is: {content}. " + + messages[-1]["content"] += (prefix_prompt + tool_prompt + suffix_prompt) + return messages + + def parse_json_format(self, message: str) -> str: + json_array_pattern = r'\[\s*\{.*?\}\s*\]' + json_object_pattern = r'\{\s*.*?\s*\}' + + match_array = re.search(json_array_pattern, message) + + if match_array: + json_array_substring = match_array.group(0) + + try: + json_array_data = json.loads(json_array_substring) + return json.dumps(json_array_data) + except json.JSONDecodeError: + pass + + match_object = re.search(json_object_pattern, message) + + if match_object: + json_object_substring = match_object.group(0) + + try: + json_object_data = json.loads(json_object_substring) + return json.dumps(json_object_data) + except json.JSONDecodeError: + pass + return '[]' + + def parse_tool_calls(self, message): + # add tool call id and type for models don't support tool call + tool_calls = json.loads(self.parse_json_format(message)) + for tool_call in tool_calls: + tool_call["id"] = generator_tool_call_id() + tool_call["type"] = "function" + return tool_calls + + def execute(self, query: Query): + return self.process(query) + + @abstractmethod + def process(self, query: Query): + raise NotImplementedError \ No newline at end of file diff --git a/cerebrum/llm/providers/api/anthropic.py b/cerebrum/llm/providers/api/anthropic.py new file mode 100644 index 00000000..990af057 --- /dev/null +++ b/cerebrum/llm/providers/api/anthropic.py @@ -0,0 +1,141 @@ +import re +import json +import anthropic +from typing import List, Dict, Any + +from cerebrum.llm.base import BaseLLM +from cerebrum.utils.chat import Query, Response + +class ClaudeLLM(BaseLLM): + """ + ClaudeLLM class for interacting with Anthropic's Claude models. + + This class provides methods for processing queries using Claude models, + including handling of tool calls and message formatting. + + Attributes: + model (anthropic.Anthropic): The Anthropic client for API calls. + tokenizer (None): Placeholder for tokenizer, not used in this implementation. + """ + + def __init__(self, llm_name: str, + max_gpu_memory: Dict[int, str] = None, + eval_device: str = None, + max_new_tokens: int = 256): + """ + Initialize the ClaudeLLM instance. + + Args: + llm_name (str): Name of the Claude model to use. + max_gpu_memory (Dict[int, str], optional): GPU memory configuration. + eval_device (str, optional): Device for evaluation. + max_new_tokens (int, optional): Maximum number of new tokens to generate. + log_mode (str, optional): Logging mode, defaults to "console". + """ + super().__init__(llm_name, + max_gpu_memory=max_gpu_memory, + eval_device=eval_device, + max_new_tokens=max_new_tokens,) + + def load_llm_and_tokenizer(self) -> None: + """ + Load the Anthropic client for API calls. + """ + self.model = anthropic.Anthropic() + self.tokenizer = None + + def process(self, query: Query): + """ + Process a query using the Claude model. + + Args: + agent_process (Any): The agent process containing the query and tools. + temperature (float, optional): Sampling temperature for generation. + + Raises: + AssertionError: If the model name doesn't contain 'claude'. + anthropic.APIError: If there's an error with the Anthropic API call. + Exception: For any other unexpected errors. + """ + assert re.search(r'claude', self.model_name, re.IGNORECASE), "Model name must contain 'claude'" + messages = query.messages + tools = query.tools + + print(f"{messages}", level="info") + + if tools: + messages = self.tool_calling_input_format(messages, tools) + + anthropic_messages = self._convert_to_anthropic_messages(messages) + + try: + response = self.model.messages.create( + model=self.model_name, + messages=anthropic_messages, + max_tokens=self.max_new_tokens, + temperature=0.0 + ) + + response_message = response.content[0].text + # self.logger.log(f"API Response: {response_message}", level="info") + tool_calls = self.parse_tool_calls(response_message) if tools else None + + return Response( + response_message=response_message, + tool_calls=tool_calls + ) + except anthropic.APIError as e: + error_message = f"Anthropic API error: {str(e)}" + self.logger.log(error_message, level="warning") + return Response( + response_message=f"Error: {str(e)}", + tool_calls=None + ) + except Exception as e: + error_message = f"Unexpected error: {str(e)}" + self.logger.log(error_message, level="warning") + return Response( + response_message=f"Unexpected error: {str(e)}", + tool_calls=None + ) + + + def _convert_to_anthropic_messages(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]: + """ + Convert messages to the format expected by the Anthropic API. + + Args: + messages (List[Dict[str, str]]): Original messages. + + Returns: + List[Dict[str, str]]: Converted messages for Anthropic API. + """ + anthropic_messages = [] + for message in messages: + if message['role'] == 'system': + anthropic_messages.append({"role": "user", "content": f"System: {message['content']}"}) + anthropic_messages.append({"role": "assistant", "content": "Understood. I will follow these instructions."}) + else: + anthropic_messages.append({ + "role": "user" if message['role'] == 'user' else "assistant", + "content": message['content'] + }) + return anthropic_messages + + def tool_calling_output_format(self, tool_calling_messages: str) -> List[Dict[str, Any]]: + """ + Parse the tool calling output from the model's response. + + Args: + tool_calling_messages (str): The model's response containing tool calls. + + Returns: + List[Dict[str, Any]]: Parsed tool calls, or None if parsing fails. + """ + try: + json_content = json.loads(tool_calling_messages) + if isinstance(json_content, list) and len(json_content) > 0 and 'name' in json_content[0]: + return json_content + except json.JSONDecodeError: + pass + return super().tool_calling_output_format(tool_calling_messages) \ No newline at end of file diff --git a/cerebrum/llm/providers/api/bedrock.py b/cerebrum/llm/providers/api/bedrock.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/llm/providers/api/google.py b/cerebrum/llm/providers/api/google.py new file mode 100644 index 00000000..930bd049 --- /dev/null +++ b/cerebrum/llm/providers/api/google.py @@ -0,0 +1,101 @@ +# wrapper around gemini from google for LLMs + +import re +import time +import json + +from cerebrum.llm.base import BaseLLM +from cerebrum.utils.chat import Query, Response +from cerebrum.utils.llm import get_from_env + + +class GeminiLLM(BaseLLM): + def __init__(self, llm_name: str, + max_gpu_memory: dict = None, + eval_device: str = None, + max_new_tokens: int = 256,): + super().__init__(llm_name, + max_gpu_memory, + eval_device, + max_new_tokens,) + + def load_llm_and_tokenizer(self) -> None: + """ dynamic loading because the module is only needed for this case """ + assert re.search(r'gemini', self.model_name, re.IGNORECASE) + try: + import google.generativeai as genai + gemini_api_key = get_from_env("GEMINI_API_KEY") + genai.configure(api_key=gemini_api_key) + self.model = genai.GenerativeModel(self.model_name) + self.tokenizer = None + except ImportError: + raise ImportError( + "Could not import google.generativeai python package. " + "Please install it with `pip install google-generativeai`." + ) + + def convert_messages(self, messages): + if messages: + gemini_messages = [] + for m in messages: + gemini_messages.append( + { + "role": "user" if m["role"] in ["user", "system"] else "model", + "parts": {"text": m["content"]} + } + ) + else: + gemini_messages = None + return gemini_messages + + def process(self, query: Query): + # ensures the model is the current one + """ wrapper around functions""" + + # agent_process.set_status("executing") + # agent_process.set_start_time(time.time()) + messages = query.messages + tools = query.tools + message_return_type = query.message_return_type + + if tools: + messages = self.tool_calling_input_format(messages, tools) + + # convert role to fit the gemini role types + messages = self.convert_messages( + messages=messages, + ) + + # self.logger.log( + # f"{agent_process.agent_name} is switched to executing.\n", + # level = "executing" + # ) + + outputs = self.model.generate_content( + json.dumps({"contents": messages}) + ) + + try: + result = outputs.candidates[0].content.parts[0].text + if tools: + tool_calls = self.parse_tool_calls(result) + if tool_calls: + return Response( + response_message=None, + tool_calls=tool_calls + ) + + else: + return Response( + response_message=result, + ) + + else: + if message_return_type == "json": + result = self.parse_json_format(result) + return Response( + response_message=result, + ) + except IndexError: + raise IndexError( + f"{self.model_name} can not generate a valid result, please try again") diff --git a/cerebrum/llm/providers/api/groq.py b/cerebrum/llm/providers/api/groq.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/llm/providers/api/openai.py b/cerebrum/llm/providers/api/openai.py new file mode 100644 index 00000000..a6e002f1 --- /dev/null +++ b/cerebrum/llm/providers/api/openai.py @@ -0,0 +1,100 @@ +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 + + +class GPTLLM(BaseLLM): + def __init__(self, llm_name: str, + max_gpu_memory: dict = None, + eval_device: str = None, + max_new_tokens: int = 1024,): + super().__init__(llm_name, + max_gpu_memory, + eval_device, + max_new_tokens,) + + def load_llm_and_tokenizer(self) -> None: + self.model = OpenAI() + self.tokenizer = None + + def parse_tool_calls(self, tool_calls): + if tool_calls: + parsed_tool_calls = [] + for tool_call in tool_calls: + function_name = tool_call.function.name + function_args = json.loads(tool_call.function.arguments) + parsed_tool_calls.append( + { + "name": function_name, + "parameters": function_args, + "type": tool_call.type, + "id": tool_call.id + } + ) + return parsed_tool_calls + return None + + def process(self, query: Query): + # ensures the model is the current one + assert re.search(r'gpt', self.model_name, re.IGNORECASE) + + """ wrapper around openai api """ + # agent_process.set_status("executing") + # agent_process.set_start_time(time.time()) + messages = query.messages + # print(messages) + # self.logger.log( + # f"{agent_process.agent_name} is switched to executing.\n", + # level = "executing" + # ) + # time.sleep(10) + try: + response = self.model.chat.completions.create( + model=self.model_name, + messages=messages, + tools=query.tools, + # tool_choice = "required" if agent_process.query.tools else None, + max_tokens=self.max_new_tokens, + # response_format = {"type": "json_object"} + ) + # print(response_message) + response_message = response.choices[0].message.content + # print(response_message) + tool_calls = self.parse_tool_calls( + response.choices[0].message.tool_calls + ) + # print(tool_calls) + # print(response.choices[0].message) + return Response( + response_message=response_message, + tool_calls=tool_calls + ) + except openai.APIConnectionError as e: + return Response( + response_message=f"Server connection error: {e.__cause__}" + ) + + except openai.RateLimitError as e: + return Response( + response_message=f"OpenAI RATE LIMIT error {e.status_code}: (e.response)" + ) + except openai.APIStatusError as e: + return Response( + response_message=f"OpenAI STATUS error {e.status_code}: (e.response)" + ) + except openai.BadRequestError as e: + return Response( + response_message=f"OpenAI BAD REQUEST error {e.status_code}: (e.response)" + ) + except Exception as e: + return Response( + response_message=f"An unexpected error occurred: {e}" + ) diff --git a/cerebrum/llm/providers/local/huggingface.py b/cerebrum/llm/providers/local/huggingface.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/llm/providers/local/ollama.py b/cerebrum/llm/providers/local/ollama.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/llm/providers/local/vllm.py b/cerebrum/llm/providers/local/vllm.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/llm/registry.py b/cerebrum/llm/registry.py new file mode 100644 index 00000000..7100a489 --- /dev/null +++ b/cerebrum/llm/registry.py @@ -0,0 +1,33 @@ +# registering all proprietary llm models in a constant + + + +#used for closed LLM model registry +from cerebrum.llm.providers.api.anthropic import ClaudeLLM +from cerebrum.llm.providers.api.google import GeminiLLM +from cerebrum.llm.providers.api.openai import GPTLLM + + +API_MODEL_REGISTRY = { + # Gemini + "gemini-1.5-flash": GeminiLLM, + "gemini-1.5-pro": GeminiLLM, + + # GPT + 'gpt-3.5-turbo': GPTLLM, + 'gpt-4-turbo': GPTLLM, + 'gpt-4o': GPTLLM, + 'gpt-4o-2024-05-13': GPTLLM, + 'gpt-4o-mini': GPTLLM, + + # claude + 'claude-3-5-sonnet-20240620': ClaudeLLM, + + # amazon bedrock + # 'bedrock/anthropic.claude-3-haiku-20240307-v1:0': BedrockLLM, + + #Groq + # '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/cerebrum/manager/__init__.py b/cerebrum/manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cerebrum/manager/manager.py b/cerebrum/manager/manager.py new file mode 100644 index 00000000..50c4c46f --- /dev/null +++ b/cerebrum/manager/manager.py @@ -0,0 +1,258 @@ +import importlib +import os +import json +import base64 +import subprocess +import sys +from typing import List, Dict +import requests +from pathlib import Path +import platformdirs +import importlib.util + +from cerebrum.manager.package import AgentPackage +from cerebrum.utils.manager import get_newest_version + +class AgentManager: + def __init__(self, base_url: str): + self.base_url = base_url + self.base_path = os.path.dirname(os.path.abspath(__file__)) + self.cache_dir = Path(platformdirs.user_cache_dir("cerebrum")) + self.cache_dir.mkdir(parents=True, exist_ok=True) + + + def _version_to_path(self, version: str) -> str: + return version.replace('.', '-') + + def _path_to_version(self, path_version: str) -> str: + return path_version.replace('-', '.') + + def package_agent(self, folder_path: str) -> Dict: + agent_files = self._get_agent_files(folder_path) + metadata = self._get_agent_metadata(folder_path) + + return { + "author": metadata.get("meta", {}).get('author'), + "name": metadata.get('name'), + "version": metadata.get("meta", {}).get('version'), + "license": metadata.get("license", "Unknown"), + "files": agent_files, + "entry": metadata.get("build", {}).get("entry", "agent.py"), + "module": metadata.get("build", {}).get("module", "Agent") + } + + def upload_agent(self, payload: Dict): + response = requests.post(f"{self.base_url}/cerebrum/upload", json=payload) + response.raise_for_status() + print(f"Agent {payload.get('author')}/{payload.get('name')} (v{payload.get('version')}) uploaded successfully.") + + def download_agent(self, author: str, name: str, version: str | None = None) -> tuple[str, str, str]: + if version is None: + cached_versions = self._get_cached_versions(author, name) + version = get_newest_version(cached_versions) + + cache_path = self._get_cache_path(author, name, version) + + if cache_path.exists(): + print(f"Using cached version of {author}/{name} (v{version})") + return author, name, version + + if version is None: + params = { + "author": author, + "name": name, + } + else: + params = { + "author": author, + "name": name, + "version": version + } + + response = requests.get(f"{self.base_url}/cerebrum/download", params=params) + response.raise_for_status() + agent_data = response.json() + + actual_version = agent_data.get('version', version) + cache_path = self._get_cache_path(author, name, actual_version) + print(cache_path) + + self._save_agent_to_cache(agent_data, cache_path) + print( + f"Agent {author}/{name} (v{actual_version}) downloaded and cached successfully.") + + if not self.check_reqs_installed(cache_path): + self.install_agent_reqs(cache_path) + + return author, name, actual_version + + def _get_cached_versions(self, author: str, name: str) -> List[str]: + agent_dir = self.cache_dir / author / name + if agent_dir.exists(): + return [self._path_to_version(v.stem) for v in agent_dir.glob("*.agent") if v.is_file()] + return [] + + def _get_cache_path(self, author: str, name: str, version: str) -> Path: + return self.cache_dir / author / name / f"{self._version_to_path(version)}.agent" + + def _save_agent_to_cache(self, agent_data: Dict, cache_path: Path): + agent_package = AgentPackage(cache_path) + agent_package.metadata = { + "author": agent_data["author"], + "name": agent_data["name"], + "version": agent_data["version"], + "license": agent_data["license"], + "entry": agent_data["entry"], + "module": agent_data["module"] + } + agent_package.files = {file["path"]: base64.b64decode(file["content"]) for file in agent_data["files"]} + agent_package.save() + + # Ensure the cache directory exists + cache_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"Saved agent to cache: {cache_path}") + + def _get_agent_files(self, folder_path: str) -> List[Dict[str, str]]: + files = [] + for root, _, filenames in os.walk(folder_path): + for filename in filenames: + file_path = os.path.join(root, filename) + relative_path = os.path.relpath(file_path, folder_path) + with open(file_path, "rb") as f: + content = base64.b64encode(f.read()).decode('utf-8') + files.append({ + "path": relative_path, + "content": content + }) + return files + + def _get_agent_metadata(self, folder_path: str) -> Dict[str, str]: + config_path = os.path.join(folder_path, "config.json") + if os.path.exists(config_path): + with open(config_path, "r") as f: + return json.load(f) + return {} + + def list_available_agents(self) -> List[Dict[str, str]]: + response = requests.get(f"{self.base_url}/cerebrum/get_all_agents") + response.raise_for_status() + + response: dict = response.json() + + agent_list = [] + + for v in list(response.values())[:-1]: + agent_list.append({ + "agent": "/".join([v["author"], v["name"], v['version']]) + }) + + return agent_list + + def check_agent_updates(self, author: str, name: str, current_version: str) -> bool: + response = requests.get(f"{self.base_url}/cerebrum/check_updates", params={ + "author": author, + "name": name, + "current_version": current_version + }) + response.raise_for_status() + return response.json()["update_available"] + + def check_reqs_installed(self, agent_path: Path) -> bool: + agent_package = AgentPackage(agent_path) + agent_package.load() + reqs_content = agent_package.files.get("meta_requirements.txt") + if not reqs_content: + return True # No requirements file, consider it as installed + + try: + result = subprocess.run( + ['conda', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except Exception: + result = subprocess.run( + ['pip', 'list', '--format=freeze'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + reqs = [line.strip().split("==")[0] for line in reqs_content.decode('utf-8').splitlines() if line.strip() and not line.startswith("#")] + + output = result.stdout.decode('utf-8') + installed_packages = [line.split()[0] + for line in output.splitlines() if line] + + return all(req in installed_packages for req in reqs) + + def install_agent_reqs(self, agent_path: Path): + agent_package = AgentPackage(agent_path) + agent_package.load() + reqs_content = agent_package.files.get("meta_requirements.txt") + if not reqs_content: + print("No meta_requirements.txt found. Skipping dependency installation.") + return + + temp_reqs_path = self.cache_dir / "temp_requirements.txt" + with open(temp_reqs_path, "wb") as f: + f.write(reqs_content) + + log_path = agent_path.with_suffix('.log') + + print(f"Installing dependencies for agent. Writing to {log_path}") + + with open(log_path, "a") as f: + subprocess.check_call([ + sys.executable, + "-m", + "pip", + "install", + "-r", + str(temp_reqs_path) + ], stdout=f, stderr=f) + + temp_reqs_path.unlink() # Remove temporary requirements file + + def load_agent(self, author: str, name: str, version: str | None = None): + if version is None: + cached_versions = self._get_cached_versions(author, name) + version = get_newest_version(cached_versions) + + agent_path = self._get_cache_path(author, name, version) + + if not agent_path.exists(): + print(f"Agent {author}/{name} (v{version}) not found in cache. Downloading...") + self.download_agent(author, name, version) + + agent_package = AgentPackage(agent_path) + agent_package.load() + + entry_point = agent_package.get_entry_point() + module_name = agent_package.get_module_name() + + # Create a temporary directory to extract the agent files + temp_dir = self.cache_dir / "temp" / f"{author}_{name}_{version}" + temp_dir.mkdir(parents=True, exist_ok=True) + + # Extract agent files to the temporary directory + for filename, content in agent_package.files.items(): + file_path = temp_dir / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_bytes(content) + + # Add the temporary directory to sys.path + sys.path.insert(0, str(temp_dir)) + + # Load the module + spec = importlib.util.spec_from_file_location(module_name, str(temp_dir / entry_point)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Remove the temporary directory from sys.path + sys.path.pop(0) + + # Get the agent class + agent_class = getattr(module, module_name) + + return agent_class, agent_package.get_config() + + +if __name__ == '__main__': + manager = AgentManager('https://my.aios.foundation/') + agent = manager.download_agent('example', 'academic_agent') + print(agent) \ No newline at end of file diff --git a/cerebrum/manager/package.py b/cerebrum/manager/package.py new file mode 100644 index 00000000..0410e7fa --- /dev/null +++ b/cerebrum/manager/package.py @@ -0,0 +1,36 @@ +import json +from pathlib import Path +import zipfile +import os + +class AgentPackage: + def __init__(self, path: Path): + self.path = path + self.metadata = {} + self.files = {} + + def load(self): + with zipfile.ZipFile(self.path, 'r') as zip_ref: + self.metadata = json.loads(zip_ref.read('metadata.json').decode('utf-8')) + for file_info in zip_ref.infolist(): + if file_info.filename != 'metadata.json': + self.files[file_info.filename] = zip_ref.read(file_info.filename) + + def save(self): + directory = os.path.dirname(self.path) + + os.makedirs(directory, exist_ok=True) + + with zipfile.ZipFile(self.path, 'w') as zip_ref: + zip_ref.writestr('metadata.json', json.dumps(self.metadata)) + for filename, content in self.files.items(): + zip_ref.writestr(filename, content) + + def get_entry_point(self): + return self.metadata.get('entry', 'agent.py') + + def get_module_name(self): + return self.metadata.get('module', 'Agent') + + def get_config(self): + return self.metadata \ No newline at end of file diff --git a/cerebrum/runtime/__init__.py b/cerebrum/runtime/__init__.py new file mode 100644 index 00000000..4165f92e --- /dev/null +++ b/cerebrum/runtime/__init__.py @@ -0,0 +1,55 @@ + +# from cerebrum.agents.base import BaseAgent +# from cerebrum.llm.base import BaseLLM + + +from cerebrum.runtime.process import RunnableAgent + + +class Pipeline: + def __init__(self): + self.agent_classes = dict() + self.llms = dict() + self.running_processes = [] + self.responses = [] + + + def add_agent(self, agent_class, config, order: int): + self.agent_classes[order] = { + 'agent_class': agent_class, + 'config': config + } + + return self + + def add_llm(self, llm, order: int): + self.llms[order] = llm + return self + + + def run(self, query: str): + agent_keys, llm_keys = list(self.agent_classes.keys()), list(self.llms.keys()) + + if len(agent_keys) != len (llm_keys): + return False + + agent_keys.sort() + + for k in agent_keys: + # support single step pipelines for now + if k != agent_keys[-1]: + return False + else: + _agent = RunnableAgent( + self.agent_classes.get(k).get('agent_class'), + self.agent_classes.get(k).get('config'), + self.llms.get(k) + ) + + self.running_processes.append(_agent) + + for process in self.running_processes: + res = _agent.run(query) + self.responses.append(res) + + return self.responses[0] diff --git a/cerebrum/runtime/process.py b/cerebrum/runtime/process.py new file mode 100644 index 00000000..b8363e72 --- /dev/null +++ b/cerebrum/runtime/process.py @@ -0,0 +1,40 @@ +from typing import Any, Type + +# from cerebrum.agents.base import BaseAgent +# from cerebrum.llm.base import BaseLLM +from cerebrum.utils.chat import Query + +class RunnableAgent: + def __init__(self, agent_class, config, llm): + self.agent = agent_class + self.config = config + self.llm = llm + + def run(self, query): + _runnable = AgentProcessor.make_runnable( + self.agent, + query, + self.config + ) + + _runnable.llm = self.llm + + return _runnable.run() + +class AgentProcessor: + @staticmethod + def process_response(query: Query, llm: Any): + print(query) + print(llm) + return llm.execute(query) + + @staticmethod + def make_runnable(agent_class: Type[Any], query: str, config: dict): + _agent = agent_class( + 'test', + query, + config + ) + + return _agent + diff --git a/cerebrum/tools/arxiv/arxiv.py b/cerebrum/tools/arxiv/arxiv.py new file mode 100644 index 00000000..56dccc18 --- /dev/null +++ b/cerebrum/tools/arxiv/arxiv.py @@ -0,0 +1,120 @@ +import re + +from ..base import BaseTool + +import arxiv + +from typing import Optional + +class Arxiv(BaseTool): + """Arxiv Tool, refactored from Langchain. + + To use, you should have the ``arxiv`` python package installed. + https://lukasschwab.me/arxiv.py/index.html + This wrapper will use the Arxiv API to conduct searches and + fetch document summaries. By default, it will return the document summaries + of the top-k results. + If the query is in the form of arxiv identifier + (see https://info.arxiv.org/help/find/index.html), it will return the paper + corresponding to the arxiv identifier. + It limits the Document content by doc_content_chars_max. + Set doc_content_chars_max=None if you don't want to limit the content size. + + Attributes: + top_k_results: number of the top-scored document used for the arxiv tool + ARXIV_MAX_QUERY_LENGTH: the cut limit on the query used for the arxiv tool. + load_max_docs: a limit to the number of loaded documents + load_all_available_meta: + if True: the `metadata` of the loaded Documents contains all available + meta info (see https://lukasschwab.me/arxiv.py/index.html#Result), + if False: the `metadata` contains only the published date, title, + authors and summary. + doc_content_chars_max: an optional cut limit for the length of a document's + content + """ + + def __init__(self): + super().__init__() + self.top_k_results: int = 3 + self.ARXIV_MAX_QUERY_LENGTH: int = 300 + self.load_max_docs: int = 100 + self.load_all_available_meta: bool = False + self.doc_content_chars_max: Optional[int] = 4000 + self.build_client() + + def is_arxiv_identifier(self, query: str) -> bool: + """Check if a query is an arxiv identifier.""" + arxiv_identifier_pattern = r"\d{2}(0[1-9]|1[0-2])\.\d{4,5}(v\d+|)|\d{7}.*" + for query_item in query[: self.ARXIV_MAX_QUERY_LENGTH].split(): + match_result = re.match(arxiv_identifier_pattern, query_item) + if not match_result: + return False + assert match_result is not None + if not match_result.group(0) == query_item: + return False + return True + + def build_client(self): + """Validate that the python package exists in environment.""" + self.arxiv_search = arxiv.Search + self.arxiv_exceptions = arxiv.ArxivError + + def run(self, params) -> str: + """ + Performs an arxiv search and A single string + with the publish date, title, authors, and summary + for each article separated by two newlines. + + If an error occurs or no documents found, error text + is returned instead. Wrapper for + https://lukasschwab.me/arxiv.py/index.html#Search + + Args: + query: a plaintext search query + """ # noqa: E501 + query = params["query"] + try: + if self.is_arxiv_identifier(query): + results = self.arxiv_search( + id_list=query.split(), + max_results=self.top_k_results, + ).results() + else: + results = self.arxiv_search( # type: ignore + query[: self.ARXIV_MAX_QUERY_LENGTH], max_results=self.top_k_results + ).results() + except self.arxiv_exceptions as ex: + return f"Arxiv exception: {ex}" + docs = [ + f"Published: {result.updated.date()}\n" + f"Title: {result.title}\n" + f"Authors: {', '.join(a.name for a in result.authors)}\n" + f"Summary: {result.summary}" + for result in results + ] + if docs: + return "\n\n".join(docs)[: self.doc_content_chars_max] + else: + return "No good Arxiv Result was found" + + def get_tool_call_format(self): + tool_call_format = { + "type": "function", + "function": { + "name": "arxiv", + "description": "Query articles or topics in arxiv", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Input query that describes what to search in arxiv" + } + }, + "required": [ + "query" + ] + } + } + } + return tool_call_format \ No newline at end of file diff --git a/cerebrum/tools/base.py b/cerebrum/tools/base.py new file mode 100644 index 00000000..f17b4ccf --- /dev/null +++ b/cerebrum/tools/base.py @@ -0,0 +1,45 @@ +class BaseTool: + """Base class for calling tools + """ + def __init__(self) -> None: + pass + + def run(self, params) -> None: + pass + + def get_tool_call_format(self) -> dict: + """Get tool calling format following the function calling from OpenAI: https://platform.openai.com/docs/guides/function-calling + """ + pass + +class BaseRapidAPITool(BaseTool): + """Base class for calling tools from rapidapi hub: https://rapidapi.com/hub + """ + def __init__(self): + super().__init__() + self.url: str = None + self.host_name: str = None + self.api_key: str = None + + def run(self, params: dict): + pass + + def get_tool_call_format(self) -> dict: + pass + + +class BaseHuggingfaceTool(BaseTool): + """Base class for calling models from huggingface + + """ + def __init__(self): + super().__init__() + self.url: str = None + self.host_name: str = None + self.api_key: str = None + + def run(self, params: dict): + pass + + def get_tool_call_format(self) -> dict: + pass \ No newline at end of file diff --git a/cerebrum/utils/chat.py b/cerebrum/utils/chat.py new file mode 100644 index 00000000..442eea2a --- /dev/null +++ b/cerebrum/utils/chat.py @@ -0,0 +1,37 @@ +class Query: + def __init__(self, + messages, + tools = None, + message_return_type = "json" + ) -> None: + """Query format + + Args: + messages (list): + [ + {"role": "xxx", content_key: content_value} + ] + tools (optional): tools that are used for function calling. Defaults to None. + """ + self.messages = messages + self.tools = tools + self.message_return_type = message_return_type + +class Response: + def __init__( + self, + response_message, + tool_calls: list = None + ) -> None: + """Response format + + Args: + response_message (str): "generated_text" + tool_calls (list, optional): + [ + {"name": "xxx", "parameters": {}} + ]. + Default to None. + """ + self.response_message = response_message + self.tool_calls = tool_calls \ No newline at end of file diff --git a/cerebrum/utils/llm.py b/cerebrum/utils/llm.py new file mode 100644 index 00000000..c228b8fe --- /dev/null +++ b/cerebrum/utils/llm.py @@ -0,0 +1,21 @@ +import random +import os +from typing import Optional + + +def generator_tool_call_id(): + """generate tool call id + """ + return str(random.randint(0, 1000)) + +def get_from_env(env_key: str, default: Optional[str] = None) -> str: + """Get a value from an environment variable.""" + if env_key in os.environ and os.environ[env_key]: + return os.environ[env_key] + elif default is not None: + return default + else: + raise ValueError( + f"Did not find {env_key}, please add an environment variable" + f" `{env_key}` which contains it. " + ) \ No newline at end of file diff --git a/cerebrum/utils/manager.py b/cerebrum/utils/manager.py new file mode 100644 index 00000000..09919de4 --- /dev/null +++ b/cerebrum/utils/manager.py @@ -0,0 +1,19 @@ +import functools + +def compare_versions(version1, version2): + v1_parts = [int(part) for part in version1.split('.')] + v2_parts = [int(part) for part in version2.split('.')] + + for i in range(max(len(v1_parts), len(v2_parts))): + v1 = v1_parts[i] if i < len(v1_parts) else 0 + v2 = v2_parts[i] if i < len(v2_parts) else 0 + + if v1 > v2: + return 1 + elif v1 < v2: + return -1 + + return 0 + +def get_newest_version(version_list): + return max(version_list, key=functools.cmp_to_key(compare_versions)) \ No newline at end of file diff --git a/launch.py b/launch.py index 3f6aa63f..6b6bd274 100644 --- a/launch.py +++ b/launch.py @@ -65,7 +65,7 @@ def run_npm(open: bool=False): if __name__ == "__main__": start_server() - run_npm(True) + # run_npm(True) try: while True: diff --git a/requirements.txt b/requirements.txt index 43586456..6b1159fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,12 @@ anthropic python-dotenv litellm pyfzf -getch +# getch npm platformdirs arxiv llama-index-embeddings-huggingface watchdog -chromadb \ No newline at end of file +chromadb +llama_index==0.10.19 +llama_index_core==0.10.19 \ No newline at end of file